{"action":"created","comment":{"url":"https://api.github.com/repos/aspnet/Razor/pulls/comments/29997777","id":29997777,"diff_hunk":"@@ -183,112 +197,164 @@ protected virtual string GenerateUniqueId()\n                 }\n \n                 // Render all of the bound attribute values for the tag helper.\n-                RenderBoundHTMLAttributes(chunk.Attributes,\n-                                          tagHelperVariableName,\n-                                          tagHelperDescriptor.Attributes,\n-                                          htmlAttributeValues);\n+                RenderBoundHTMLAttributes(\n+                    chunk.Attributes,\n+                    tagHelperVariableName,\n+                    tagHelperDescriptor.Attributes,\n+                    boundStringAttributeNames,\n+                    htmlAttributeValues);\n             }\n         }\n \n-        private void RenderBoundHTMLAttributes(IList<KeyValuePair<string, Chunk>> chunkAttributes,\n-                                               string tagHelperVariableName,\n-                                               IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,\n-                                               Dictionary<string, string> htmlAttributeValues)\n+        private void RenderBoundHTMLAttributes(\n+            IList<KeyValuePair<string, Chunk>> chunkAttributes,\n+            string tagHelperVariableName,\n+            IEnumerable<TagHelperAttributeDescriptor> attributeDescriptors,\n+            HashSet<string> boundStringAttributeNames,\n+            Dictionary<string, string> htmlAttributeValues)\n         {\n+            // Pass 1: Initialize all dictionary-typed properties we can (if they're null at runtime).\n+            foreach (var attributeDescriptor in attributeDescriptors)\n+            {\n+                if (!string.IsNullOrEmpty(attributeDescriptor.ObjectCreationExpression))\n+                {\n+                    var valueAccessor = string.Format(\n+                        CultureInfo.InvariantCulture,\n+                        \"{0}.{1}\",\n+                        tagHelperVariableName,\n+                        attributeDescriptor.PropertyName);\n+\n+                    _writer\n+                        .Write(\"if (\")\n+                        .Write(valueAccessor)\n+                        .WriteLine(\" == null)\");\n+                    using (_writer.BuildScope())\n+                    {\n+                        _writer\n+                            .WriteStartAssignment(valueAccessor)\n+                            .Write(attributeDescriptor.ObjectCreationExpression)\n+                            .WriteLine(\";\");\n+                    }\n+                }\n+            }\n+\n+            // Pass 2: Set all individually-bound properties.\n+            var attributeNames = chunkAttributes.Select(kvp => kvp.Key);\n+            var remainingAttributeNames = new HashSet<string>(attributeNames, StringComparer.OrdinalIgnoreCase);\n             foreach (var attributeDescriptor in attributeDescriptors)\n             {\n                 var matchingAttributes = chunkAttributes.Where(\n-                    attr => string.Equals(attr.Key, attributeDescriptor.Name, StringComparison.OrdinalIgnoreCase));\n+                    kvp => string.Equals(kvp.Key, attributeDescriptor.Name, StringComparison.OrdinalIgnoreCase));\n \n                 if (matchingAttributes.Any())\n                 {\n-                    // First attribute wins, even if there's duplicates.\n-                    var attributeValueChunk = matchingAttributes.First().Value;\n+                    remainingAttributeNames.Remove(attributeDescriptor.Name);\n+\n+                    // We capture the tag helpers property value accessor so we can retrieve it later (if we need to).\n+                    var valueAccessor = string.Format(\n+                        CultureInfo.InvariantCulture,\n+                        \"{0}.{1}\",\n+                        tagHelperVariableName,\n+                        attributeDescriptor.PropertyName);\n \n+                    // If we haven't recorded this attribute value before then we need to record its value.\n                     var attributeValueRecorded = htmlAttributeValues.ContainsKey(attributeDescriptor.Name);\n+                    if (!attributeValueRecorded)\n+                    {\n+                        // We only need to create attribute values once per HTML element (not once per tag helper).\n+                        // We're saving the value accessor so we can retrieve it later if there are more tag\n+                        // helpers that need the value.\n+                        htmlAttributeValues.Add(attributeDescriptor.Name, valueAccessor);\n \n-                    // Bufferable attributes are attributes that can have Razor code inside of them.\n-                    var bufferableAttribute = IsStringAttribute(attributeDescriptor);\n+                        // First attribute wins, even if there are duplicates.\n+                        var attributeValueChunk = matchingAttributes.First().Value;\n \n-                    // Plain text values are non Razor code (@DateTime.Now) values. If an attribute is bufferable it\n-                    // may be more than just a plain text value, it may also contain Razor code which is why we attempt\n-                    // to retrieve a plain text value here.\n-                    string textValue;\n-                    var isPlainTextValue = TryGetPlainTextValue(attributeValueChunk, out textValue);\n+                        // Bufferable attributes are attributes that can have Razor code inside of them. Such\n+                        // attributes have string values and may be calculated using a temporary TextWriter or other\n+                        // buffer.\n+                        var bufferableAttribute = boundStringAttributeNames.Contains(attributeDescriptor.Name);\n \n-                    // If we haven't recorded a value and we need to buffer an attribute value and the value is not\n-                    // plain text then we need to prepare the value prior to setting it below.\n-                    if (!attributeValueRecorded && bufferableAttribute && !isPlainTextValue)\n+                        RenderNewAttributeValueAssignment(\n+                            attributeDescriptor,\n+                            bufferableAttribute,\n+                            attributeValueChunk,\n+                            valueAccessor);\n+\n+                        // Execution contexts are a runtime feature.\n+                        if (_designTimeMode)\n+                        {\n+                            continue;\n+                        }\n+\n+                        // We need to inform the context of the attribute value.\n+                        _writer\n+                            .WriteStartInstanceMethodInvocation(\n+                                ExecutionContextVariableName,\n+                                _tagHelperContext.ExecutionContextAddTagHelperAttributeMethodName)\n+                            .WriteStringLiteral(attributeDescriptor.Name)\n+                            .WriteParameterSeparator()\n+                            .Write(valueAccessor)\n+                            .WriteEndMethodInvocation();\n+                    }\n+                    else\n                     {\n-                        BuildBufferedWritingScope(attributeValueChunk, htmlEncodeValues: false);\n+                        // The attribute value has already been recorded, lets retrieve it from the stored value\n+                        // accessors.\n+                        _writer\n+                            .WriteStartAssignment(valueAccessor)\n+                            .Write(htmlAttributeValues[attributeDescriptor.Name])\n+                            .WriteLine(\";\");\n                     }\n+                }\n+            }\n \n-                    // We capture the tag helpers property value accessor so we can retrieve it later (if we need to).\n-                    var valueAccessor = string.Format(CultureInfo.InvariantCulture,\n-                                                      \"{0}.{1}\",\n-                                                      tagHelperVariableName,\n-                                                      attributeDescriptor.PropertyName);\n+            // Pass 3: Add all remaining attributes that match a prefix to corresponding dictionary. Use same order as\n+            // above loops but skip everything already bound to a specific property of this tag helper.\n+            foreach (var attributeDescriptor in attributeDescriptors)","path":"src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs","position":211,"original_position":211,"commit_id":"b3f36b9b8e9a462b8f4b499c0a67eaf66e20ac19","original_commit_id":"b3f36b9b8e9a462b8f4b499c0a67eaf66e20ac19","user":{"login":"dougbu","id":6431421,"avatar_url":"https://avatars.githubusercontent.com/u/6431421?v=3","gravatar_id":"","url":"https://api.github.com/users/dougbu","html_url":"https://github.com/dougbu","followers_url":"https://api.github.com/users/dougbu/followers","following_url":"https://api.github.com/users/dougbu/following{/other_user}","gists_url":"https://api.github.com/users/dougbu/gists{/gist_id}","starred_url":"https://api.github.com/users/dougbu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dougbu/subscriptions","organizations_url":"https://api.github.com/users/dougbu/orgs","repos_url":"https://api.github.com/users/dougbu/repos","events_url":"https://api.github.com/users/dougbu/events{/privacy}","received_events_url":"https://api.github.com/users/dougbu/received_events","type":"User","site_admin":false},"body":"When doing #225, will still do Pass 1.  Passes 2 and 3 would instead be a per-tag-helper precedence applied while going through the attributes.\r\n\r\nAlternatively could add a Pass 4 for #225: Reorder the attribute lists created earlier to match the source order.","created_at":"2015-05-10T01:13:26Z","updated_at":"2015-05-10T01:13:26Z","html_url":"https://github.com/aspnet/Razor/pull/380#discussion_r29997777","pull_request_url":"https://api.github.com/repos/aspnet/Razor/pulls/380","_links":{"self":{"href":"https://api.github.com/repos/aspnet/Razor/pulls/comments/29997777"},"html":{"href":"https://github.com/aspnet/Razor/pull/380#discussion_r29997777"},"pull_request":{"href":"https://api.github.com/repos/aspnet/Razor/pulls/380"}}},"pull_request":{"url":"https://api.github.com/repos/aspnet/Razor/pulls/380","id":34975654,"html_url":"https://github.com/aspnet/Razor/pull/380","diff_url":"https://github.com/aspnet/Razor/pull/380.diff","patch_url":"https://github.com/aspnet/Razor/pull/380.patch","issue_url":"https://api.github.com/repos/aspnet/Razor/issues/380","number":380,"state":"open","locked":false,"title":"[Design] Add `[HtmlAttributeName(..., DictionaryAttributePrefix=\"prefix\")]`","user":{"login":"dougbu","id":6431421,"avatar_url":"https://avatars.githubusercontent.com/u/6431421?v=3","gravatar_id":"","url":"https://api.github.com/users/dougbu","html_url":"https://github.com/dougbu","followers_url":"https://api.github.com/users/dougbu/followers","following_url":"https://api.github.com/users/dougbu/following{/other_user}","gists_url":"https://api.github.com/users/dougbu/gists{/gist_id}","starred_url":"https://api.github.com/users/dougbu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dougbu/subscriptions","organizations_url":"https://api.github.com/users/dougbu/orgs","repos_url":"https://api.github.com/users/dougbu/repos","events_url":"https://api.github.com/users/dougbu/events{/privacy}","received_events_url":"https://api.github.com/users/dougbu/received_events","type":"User","site_admin":false},"body":"- #89\r\n- support adding attributes (that aren't otherwise bound) to a tag helper dictionary\r\n- add `ObjectCreationExpression`, `Prefix` and more to `TagHelperAttributeDescriptor`\r\n - determine `string`-ness when creating `TagHelperAttributeDescriptor`s\r\n- handle `Prefix` matches in `TagHelperBlockRewriter`\r\n- refactor `CSharpTagHelperCodeRenderer` to handle new indexer property assignments\r\n - calculate `string`-ness for a bound attribute using all `TagHelperAttributeDescriptor`s\r\n- extend handling of invalid attribute names to include `[HtmlAttributeName]`\r\n\r\nknown issue: `ObjectCreationExpression` is a C# expression because otherwise\r\ngeneration has insufficient information to determine correct constructor.\r\n- no VB or F# (for example) workarounds today\r\n\r\nno new tests yet\r\n\r\nnits:\r\n- add `TagHelperHelper`\r\n- separate `TagHelperAttributeDescriptorComparer` and `TypeBasedTagHelperDescriptorComparer`\r\n - encourages reuse and most are already used in multiple classes\r\n- use `<inheritdoc/>` in `TagHelperDescriptorComparer`\r\n - also give it an explicit constructor","created_at":"2015-05-08T02:19:49Z","updated_at":"2015-05-10T01:13:26Z","closed_at":null,"merged_at":null,"merge_commit_sha":"82362b542dfc875da92f0f479eb716536e6e47cd","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/aspnet/Razor/pulls/380/commits","review_comments_url":"https://api.github.com/repos/aspnet/Razor/pulls/380/comments","review_comment_url":"https://api.github.com/repos/aspnet/Razor/pulls/comments{/number}","comments_url":"https://api.github.com/repos/aspnet/Razor/issues/380/comments","statuses_url":"https://api.github.com/repos/aspnet/Razor/statuses/b3f36b9b8e9a462b8f4b499c0a67eaf66e20ac19","head":{"label":"aspnet:dougbu/dictionary.attributes.89","ref":"dougbu/dictionary.attributes.89","sha":"b3f36b9b8e9a462b8f4b499c0a67eaf66e20ac19","user":{"login":"aspnet","id":6476660,"avatar_url":"https://avatars.githubusercontent.com/u/6476660?v=3","gravatar_id":"","url":"https://api.github.com/users/aspnet","html_url":"https://github.com/aspnet","followers_url":"https://api.github.com/users/aspnet/followers","following_url":"https://api.github.com/users/aspnet/following{/other_user}","gists_url":"https://api.github.com/users/aspnet/gists{/gist_id}","starred_url":"https://api.github.com/users/aspnet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aspnet/subscriptions","organizations_url":"https://api.github.com/users/aspnet/orgs","repos_url":"https://api.github.com/users/aspnet/repos","events_url":"https://api.github.com/users/aspnet/events{/privacy}","received_events_url":"https://api.github.com/users/aspnet/received_events","type":"Organization","site_admin":false},"repo":{"id":15835991,"name":"Razor","full_name":"aspnet/Razor","owner":{"login":"aspnet","id":6476660,"avatar_url":"https://avatars.githubusercontent.com/u/6476660?v=3","gravatar_id":"","url":"https://api.github.com/users/aspnet","html_url":"https://github.com/aspnet","followers_url":"https://api.github.com/users/aspnet/followers","following_url":"https://api.github.com/users/aspnet/following{/other_user}","gists_url":"https://api.github.com/users/aspnet/gists{/gist_id}","starred_url":"https://api.github.com/users/aspnet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aspnet/subscriptions","organizations_url":"https://api.github.com/users/aspnet/orgs","repos_url":"https://api.github.com/users/aspnet/repos","events_url":"https://api.github.com/users/aspnet/events{/privacy}","received_events_url":"https://api.github.com/users/aspnet/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/aspnet/Razor","description":"Parser and code generator for CSHTML files used in view pages for MVC web apps.","fork":false,"url":"https://api.github.com/repos/aspnet/Razor","forks_url":"https://api.github.com/repos/aspnet/Razor/forks","keys_url":"https://api.github.com/repos/aspnet/Razor/keys{/key_id}","collaborators_url":"https://api.github.com/repos/aspnet/Razor/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/aspnet/Razor/teams","hooks_url":"https://api.github.com/repos/aspnet/Razor/hooks","issue_events_url":"https://api.github.com/repos/aspnet/Razor/issues/events{/number}","events_url":"https://api.github.com/repos/aspnet/Razor/events","assignees_url":"https://api.github.com/repos/aspnet/Razor/assignees{/user}","branches_url":"https://api.github.com/repos/aspnet/Razor/branches{/branch}","tags_url":"https://api.github.com/repos/aspnet/Razor/tags","blobs_url":"https://api.github.com/repos/aspnet/Razor/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/aspnet/Razor/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/aspnet/Razor/git/refs{/sha}","trees_url":"https://api.github.com/repos/aspnet/Razor/git/trees{/sha}","statuses_url":"https://api.github.com/repos/aspnet/Razor/statuses/{sha}","languages_url":"https://api.github.com/repos/aspnet/Razor/languages","stargazers_url":"https://api.github.com/repos/aspnet/Razor/stargazers","contributors_url":"https://api.github.com/repos/aspnet/Razor/contributors","subscribers_url":"https://api.github.com/repos/aspnet/Razor/subscribers","subscription_url":"https://api.github.com/repos/aspnet/Razor/subscription","commits_url":"https://api.github.com/repos/aspnet/Razor/commits{/sha}","git_commits_url":"https://api.github.com/repos/aspnet/Razor/git/commits{/sha}","comments_url":"https://api.github.com/repos/aspnet/Razor/comments{/number}","issue_comment_url":"https://api.github.com/repos/aspnet/Razor/issues/comments{/number}","contents_url":"https://api.github.com/repos/aspnet/Razor/contents/{+path}","compare_url":"https://api.github.com/repos/aspnet/Razor/compare/{base}...{head}","merges_url":"https://api.github.com/repos/aspnet/Razor/merges","archive_url":"https://api.github.com/repos/aspnet/Razor/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/aspnet/Razor/downloads","issues_url":"https://api.github.com/repos/aspnet/Razor/issues{/number}","pulls_url":"https://api.github.com/repos/aspnet/Razor/pulls{/number}","milestones_url":"https://api.github.com/repos/aspnet/Razor/milestones{/number}","notifications_url":"https://api.github.com/repos/aspnet/Razor/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/aspnet/Razor/labels{/name}","releases_url":"https://api.github.com/repos/aspnet/Razor/releases{/id}","created_at":"2014-01-12T03:52:46Z","updated_at":"2015-05-08T01:07:58Z","pushed_at":"2015-05-08T17:20:18Z","git_url":"git://github.com/aspnet/Razor.git","ssh_url":"git@github.com:aspnet/Razor.git","clone_url":"https://github.com/aspnet/Razor.git","svn_url":"https://github.com/aspnet/Razor","homepage":"","size":9752,"stargazers_count":336,"watchers_count":336,"language":"C#","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":84,"mirror_url":null,"open_issues_count":77,"forks":84,"open_issues":77,"watchers":336,"default_branch":"dev"}},"base":{"label":"aspnet:dev","ref":"dev","sha":"959616cb9c22aa68d3ca9b2ccf62b9f752d7dc2e","user":{"login":"aspnet","id":6476660,"avatar_url":"https://avatars.githubusercontent.com/u/6476660?v=3","gravatar_id":"","url":"https://api.github.com/users/aspnet","html_url":"https://github.com/aspnet","followers_url":"https://api.github.com/users/aspnet/followers","following_url":"https://api.github.com/users/aspnet/following{/other_user}","gists_url":"https://api.github.com/users/aspnet/gists{/gist_id}","starred_url":"https://api.github.com/users/aspnet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aspnet/subscriptions","organizations_url":"https://api.github.com/users/aspnet/orgs","repos_url":"https://api.github.com/users/aspnet/repos","events_url":"https://api.github.com/users/aspnet/events{/privacy}","received_events_url":"https://api.github.com/users/aspnet/received_events","type":"Organization","site_admin":false},"repo":{"id":15835991,"name":"Razor","full_name":"aspnet/Razor","owner":{"login":"aspnet","id":6476660,"avatar_url":"https://avatars.githubusercontent.com/u/6476660?v=3","gravatar_id":"","url":"https://api.github.com/users/aspnet","html_url":"https://github.com/aspnet","followers_url":"https://api.github.com/users/aspnet/followers","following_url":"https://api.github.com/users/aspnet/following{/other_user}","gists_url":"https://api.github.com/users/aspnet/gists{/gist_id}","starred_url":"https://api.github.com/users/aspnet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aspnet/subscriptions","organizations_url":"https://api.github.com/users/aspnet/orgs","repos_url":"https://api.github.com/users/aspnet/repos","events_url":"https://api.github.com/users/aspnet/events{/privacy}","received_events_url":"https://api.github.com/users/aspnet/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/aspnet/Razor","description":"Parser and code generator for CSHTML files used in view pages for MVC web apps.","fork":false,"url":"https://api.github.com/repos/aspnet/Razor","forks_url":"https://api.github.com/repos/aspnet/Razor/forks","keys_url":"https://api.github.com/repos/aspnet/Razor/keys{/key_id}","collaborators_url":"https://api.github.com/repos/aspnet/Razor/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/aspnet/Razor/teams","hooks_url":"https://api.github.com/repos/aspnet/Razor/hooks","issue_events_url":"https://api.github.com/repos/aspnet/Razor/issues/events{/number}","events_url":"https://api.github.com/repos/aspnet/Razor/events","assignees_url":"https://api.github.com/repos/aspnet/Razor/assignees{/user}","branches_url":"https://api.github.com/repos/aspnet/Razor/branches{/branch}","tags_url":"https://api.github.com/repos/aspnet/Razor/tags","blobs_url":"https://api.github.com/repos/aspnet/Razor/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/aspnet/Razor/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/aspnet/Razor/git/refs{/sha}","trees_url":"https://api.github.com/repos/aspnet/Razor/git/trees{/sha}","statuses_url":"https://api.github.com/repos/aspnet/Razor/statuses/{sha}","languages_url":"https://api.github.com/repos/aspnet/Razor/languages","stargazers_url":"https://api.github.com/repos/aspnet/Razor/stargazers","contributors_url":"https://api.github.com/repos/aspnet/Razor/contributors","subscribers_url":"https://api.github.com/repos/aspnet/Razor/subscribers","subscription_url":"https://api.github.com/repos/aspnet/Razor/subscription","commits_url":"https://api.github.com/repos/aspnet/Razor/commits{/sha}","git_commits_url":"https://api.github.com/repos/aspnet/Razor/git/commits{/sha}","comments_url":"https://api.github.com/repos/aspnet/Razor/comments{/number}","issue_comment_url":"https://api.github.com/repos/aspnet/Razor/issues/comments{/number}","contents_url":"https://api.github.com/repos/aspnet/Razor/contents/{+path}","compare_url":"https://api.github.com/repos/aspnet/Razor/compare/{base}...{head}","merges_url":"https://api.github.com/repos/aspnet/Razor/merges","archive_url":"https://api.github.com/repos/aspnet/Razor/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/aspnet/Razor/downloads","issues_url":"https://api.github.com/repos/aspnet/Razor/issues{/number}","pulls_url":"https://api.github.com/repos/aspnet/Razor/pulls{/number}","milestones_url":"https://api.github.com/repos/aspnet/Razor/milestones{/number}","notifications_url":"https://api.github.com/repos/aspnet/Razor/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/aspnet/Razor/labels{/name}","releases_url":"https://api.github.com/repos/aspnet/Razor/releases{/id}","created_at":"2014-01-12T03:52:46Z","updated_at":"2015-05-08T01:07:58Z","pushed_at":"2015-05-08T17:20:18Z","git_url":"git://github.com/aspnet/Razor.git","ssh_url":"git@github.com:aspnet/Razor.git","clone_url":"https://github.com/aspnet/Razor.git","svn_url":"https://github.com/aspnet/Razor","homepage":"","size":9752,"stargazers_count":336,"watchers_count":336,"language":"C#","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":84,"mirror_url":null,"open_issues_count":77,"forks":84,"open_issues":77,"watchers":336,"default_branch":"dev"}},"_links":{"self":{"href":"https://api.github.com/repos/aspnet/Razor/pulls/380"},"html":{"href":"https://github.com/aspnet/Razor/pull/380"},"issue":{"href":"https://api.github.com/repos/aspnet/Razor/issues/380"},"comments":{"href":"https://api.github.com/repos/aspnet/Razor/issues/380/comments"},"review_comments":{"href":"https://api.github.com/repos/aspnet/Razor/pulls/380/comments"},"review_comment":{"href":"https://api.github.com/repos/aspnet/Razor/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/aspnet/Razor/pulls/380/commits"},"statuses":{"href":"https://api.github.com/repos/aspnet/Razor/statuses/b3f36b9b8e9a462b8f4b499c0a67eaf66e20ac19"}}}}