diff --git a/Directory.Build.props b/Directory.Build.props index d37be59e20..065616f3cd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,6 +18,7 @@ MicrosoftNuGet true true + true diff --git a/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs index 982b8d4e22..bb66f15d28 100644 --- a/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs @@ -4,17 +4,13 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.AspNetCore.Mvc.Performance { @@ -22,6 +18,11 @@ namespace Microsoft.AspNetCore.Mvc.Performance { private const string DefaultRoute = "{Controller=Home}/{Action=Index}/{id?}"; + // Attribute routes can't have controller and action as parameters, so we edit the + // route template in the test to make it more realistic. + private const string ControllerReplacementToken = "{Controller=Home}"; + private const string ActionReplacementToken = "{Action=Index}"; + private MockActionDescriptorCollectionProvider _conventionalActionProvider; private MockActionDescriptorCollectionProvider _attributeActionProvider; private List _conventionalEndpointInfos; @@ -33,11 +34,11 @@ namespace Microsoft.AspNetCore.Mvc.Performance public void Setup() { _conventionalActionProvider = new MockActionDescriptorCollectionProvider( - Enumerable.Range(0, ActionCount).Select(i => CreateActionDescriptor(i, false)).ToList() + Enumerable.Range(0, ActionCount).Select(i => CreateConventionalRoutedAction(i)).ToList() ); _attributeActionProvider = new MockActionDescriptorCollectionProvider( - Enumerable.Range(0, ActionCount).Select(i => CreateActionDescriptor(i, true)).ToList() + Enumerable.Range(0, ActionCount).Select(i => CreateAttributeRoutedAction(i)).ToList() ); _conventionalEndpointInfos = new List @@ -67,27 +68,40 @@ namespace Microsoft.AspNetCore.Mvc.Performance var endpoints = endpointDataSource.Endpoints; } - private ActionDescriptor CreateActionDescriptor(int id, bool attributeRoute) + private ActionDescriptor CreateAttributeRoutedAction(int id) { - var actionDescriptor = new ActionDescriptor + var routeValues = new Dictionary(StringComparer.OrdinalIgnoreCase) { - RouteValues = new Dictionary + ["Controller"] = "Controller" + id, + ["Action"] = "Index" + }; + + var template = DefaultRoute + .Replace(ControllerReplacementToken, routeValues["Controller"]) + .Replace(ActionReplacementToken, routeValues["Action"]); + + return new ActionDescriptor + { + RouteValues = routeValues, + DisplayName = "Action " + id, + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = template, + } + }; + } + + private ActionDescriptor CreateConventionalRoutedAction(int id) + { + return new ActionDescriptor + { + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Controller"] = "Controller" + id, ["Action"] = "Index" }, DisplayName = "Action " + id }; - - if (attributeRoute) - { - actionDescriptor.AttributeRouteInfo = new AttributeRouteInfo - { - Template = DefaultRoute - }; - } - - return actionDescriptor; } private MvcEndpointDataSource CreateMvcEndpointDataSource( diff --git a/build/dependencies.props b/build/dependencies.props index 1718b94374..18cd03713f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -48,8 +48,8 @@ 3.0.0-alpha1-10099 3.0.0-alpha1-10099 3.0.0-alpha1-10099 - 3.0.0-alpha1-10099 - 3.0.0-alpha1-10099 + 3.0.0-a-alpha1-master-route-pattern-16760 + 3.0.0-a-alpha1-master-route-pattern-16760 3.0.0-alpha1-10099 3.0.0-alpha1-10099 3.0.0-alpha1-10099 diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs index a76dbe8c93..c8738bb20e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.EndpointConstraints; using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Primitives; @@ -298,20 +299,18 @@ namespace Microsoft.AspNetCore.Mvc.Internal return invoker.InvokeAsync(); }; + var defaults = new RouteValueDictionary(nonInlineDefaults); + EnsureRequiredValuesInDefaults(action.RouteValues, defaults); + var metadataCollection = BuildEndpointMetadata(action, routeName, source); var endpoint = new MatcherEndpoint( next => invokerDelegate, - template, - new RouteValueDictionary(nonInlineDefaults), + RoutePatternFactory.Parse(template, defaults, constraints: null), new RouteValueDictionary(action.RouteValues), order, metadataCollection, action.DisplayName); - // Use defaults after the endpoint is created as it merges both the inline and - // non-inline defaults into one. - EnsureRequiredValuesInDefaults(endpoint.RequiredValues, endpoint.Defaults); - return endpoint; } @@ -373,7 +372,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Required values: controller=foo, action=bar // Final constructed template: foo/bar/{category}/{id?} // Final defaults: controller=foo, action=bar, category=products - private void EnsureRequiredValuesInDefaults(RouteValueDictionary requiredValues, RouteValueDictionary defaults) + private void EnsureRequiredValuesInDefaults(IDictionary requiredValues, RouteValueDictionary defaults) { foreach (var kvp in requiredValues) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj index 7fc4891804..0698b3fd85 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Microsoft.AspNetCore.Mvc.Core/Microsoft.AspNetCore.Mvc.Core.csproj @@ -25,6 +25,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs index 83bae7b4b0..30514d5ac8 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.EndpointConstraints; using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Net.Http.Headers; using Moq; using Xunit; @@ -297,12 +298,11 @@ namespace Microsoft.AspNetCore.Mvc private MatcherEndpoint CreateEndpoint(params IEndpointConstraint[] constraints) { - EndpointMetadataCollection endpointMetadata = new EndpointMetadataCollection(constraints); + var endpointMetadata = new EndpointMetadataCollection(constraints); return new MatcherEndpoint( (r) => null, - "", - new RouteValueDictionary(), + RoutePatternFactory.Parse("/"), new RouteValueDictionary(), 0, endpointMetadata, diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs index 983af123c6..fb0df12fd3 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs @@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal Assert.Equal(displayName, matcherEndpoint.DisplayName); Assert.Equal(order, matcherEndpoint.Order); - Assert.Equal(template, matcherEndpoint.Template); + Assert.Equal(template, matcherEndpoint.RoutePattern.RawText); } [Fact] @@ -197,7 +197,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Assert var inspectors = finalEndpointTemplates - .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).Template))) + .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) .ToArray(); // Assert @@ -224,7 +224,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Assert var inspectors = finalEndpointTemplates - .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).Template))) + .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) .ToArray(); // Assert @@ -248,8 +248,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Assert Assert.Collection(endpoints, - (e) => Assert.Equal("TestController", Assert.IsType(e).Template), - (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).Template)); + (e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText), + (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText)); } [Fact] @@ -276,8 +276,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Assert Assert.Collection(endpoints1, - (e) => Assert.Equal("TestController", Assert.IsType(e).Template), - (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).Template)); + (e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText), + (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText)); Assert.Same(endpoints1, endpoints2); actionDescriptorCollectionProviderMock.VerifyGet(m => m.ActionDescriptors, Times.Once); @@ -318,8 +318,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal var endpoints = dataSource.Endpoints; Assert.Collection(endpoints, - (e) => Assert.Equal("TestController", Assert.IsType(e).Template), - (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).Template)); + (e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText), + (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText)); actionDescriptorCollectionProviderMock .Setup(m => m.ActionDescriptors) @@ -335,7 +335,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal Assert.NotSame(endpoints, newEndpoints); Assert.Collection(newEndpoints, - (e) => Assert.Equal("NewTestController/NewTestAction", Assert.IsType(e).Template)); + (e) => Assert.Equal("NewTestController/NewTestAction", Assert.IsType(e).RoutePattern.RawText)); } [Fact] @@ -357,8 +357,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Assert Assert.Collection(endpoints, - (e) => Assert.Equal("TestController/TestAction1", Assert.IsType(e).Template), - (e) => Assert.Equal("TestController/TestAction2", Assert.IsType(e).Template)); + (e) => Assert.Equal("TestController/TestAction1", Assert.IsType(e).RoutePattern.RawText), + (e) => Assert.Equal("TestController/TestAction2", Assert.IsType(e).RoutePattern.RawText)); } [Theory] @@ -381,7 +381,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal var endpoints = dataSource.Endpoints; var inspectors = finalEndpointTemplates - .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).Template))) + .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) .ToArray(); // Assert @@ -431,7 +431,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata(); Assert.NotNull(routeNameMetadata); Assert.Equal("namedRoute", routeNameMetadata.Name); - Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.Template); + Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); }, (ep) => { @@ -439,7 +439,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata(); Assert.NotNull(routeNameMetadata); Assert.Equal("namedRoute", routeNameMetadata.Name); - Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.Template); + Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText); }); } @@ -467,25 +467,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal (ep) => { var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("Home/Index/{id?}", matcherEndpoint.Template); + Assert.Equal("Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); Assert.Equal(1, matcherEndpoint.Order); }, (ep) => { var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.Template); + Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); Assert.Equal(2, matcherEndpoint.Order); }, (ep) => { var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("Products/Details/{id?}", matcherEndpoint.Template); + Assert.Equal("Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText); Assert.Equal(1, matcherEndpoint.Order); }, (ep) => { var matcherEndpoint = Assert.IsType(ep); - Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.Template); + Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText); Assert.Equal(2, matcherEndpoint.Order); }); } @@ -587,8 +587,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); - Assert.Equal("Foo/Bar", matcherEndpoint.Template); - AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); + Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText); + AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); } [Fact] @@ -609,8 +609,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); - Assert.Equal("Foo/Bar", matcherEndpoint.Template); - AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); + Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText); + AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); } [Fact] @@ -632,8 +632,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); - Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.Template); - AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); + Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.RoutePattern.RawText); + AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); } [Fact] @@ -654,8 +654,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); - Assert.Equal("Foo/Bar", matcherEndpoint.Template); - AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); + Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText); + AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); } private MvcEndpointDataSource CreateMvcEndpointDataSource( @@ -729,7 +729,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal return actionDescriptor; } - private void AssertIsSubset(RouteValueDictionary subset, RouteValueDictionary fullSet) + private void AssertIsSubset( + IReadOnlyDictionary subset, + IReadOnlyDictionary fullSet) { foreach (var subsetPair in subset) { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs index 5d52f760a7..b069feabc6 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Xunit; @@ -35,8 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing var endpoints = GetDefaultEndpoints(); endpoints.Add(new MatcherEndpoint( next => httpContext => Task.CompletedTask, - template, - new RouteValueDictionary(), + RoutePatternFactory.Parse(template), new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, @@ -51,8 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing { Endpoint = new MatcherEndpoint( next => cntxt => Task.CompletedTask, - "/", - new RouteValueDictionary(), + RoutePatternFactory.Parse("/"), new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, @@ -100,14 +99,14 @@ namespace Microsoft.AspNetCore.Mvc.Routing private List GetDefaultEndpoints() { var endpoints = new List(); - endpoints.Add(CreateEndpoint(null, "home/newaction/{id?}", new { id = "defaultid", controller = "home", action = "newaction" }, 1)); - endpoints.Add(CreateEndpoint(null, "home/contact/{id?}", new { id = "defaultid", controller = "home", action = "contact" }, 2)); - endpoints.Add(CreateEndpoint(null, "home2/newaction/{id?}", new { id = "defaultid", controller = "home2", action = "newaction" }, 3)); - endpoints.Add(CreateEndpoint(null, "home2/contact/{id?}", new { id = "defaultid", controller = "home2", action = "contact" }, 4)); - endpoints.Add(CreateEndpoint(null, "home3/contact/{id?}", new { id = "defaultid", controller = "home3", action = "contact" }, 5)); - endpoints.Add(CreateEndpoint("namedroute", "named/home/newaction/{id?}", new { id = "defaultid", controller = "home", action = "newaction" }, 6)); - endpoints.Add(CreateEndpoint("namedroute", "named/home2/newaction/{id?}", new { id = "defaultid", controller = "home2", action = "newaction" }, 7)); - endpoints.Add(CreateEndpoint("namedroute", "named/home/contact/{id?}", new { id = "defaultid", controller = "home", action = "contact" }, 8)); + endpoints.Add(CreateEndpoint(null, "home/newaction/{id}", new { id = "defaultid", controller = "home", action = "newaction" }, 1)); + endpoints.Add(CreateEndpoint(null, "home/contact/{id}", new { id = "defaultid", controller = "home", action = "contact" }, 2)); + endpoints.Add(CreateEndpoint(null, "home2/newaction/{id}", new { id = "defaultid", controller = "home2", action = "newaction" }, 3)); + endpoints.Add(CreateEndpoint(null, "home2/contact/{id}", new { id = "defaultid", controller = "home2", action = "contact" }, 4)); + endpoints.Add(CreateEndpoint(null, "home3/contact/{id}", new { id = "defaultid", controller = "home3", action = "contact" }, 5)); + endpoints.Add(CreateEndpoint("namedroute", "named/home/newaction/{id}", new { id = "defaultid", controller = "home", action = "newaction" }, 6)); + endpoints.Add(CreateEndpoint("namedroute", "named/home2/newaction/{id}", new { id = "defaultid", controller = "home2", action = "newaction" }, 7)); + endpoints.Add(CreateEndpoint("namedroute", "named/home/contact/{id}", new { id = "defaultid", controller = "home", action = "contact" }, 8)); endpoints.Add(CreateEndpoint("MyRouteName", "any/url", new { }, 9)); return endpoints; } @@ -122,8 +121,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing return new MatcherEndpoint( next => (httpContext) => Task.CompletedTask, - template, - new RouteValueDictionary(defaults), + RoutePatternFactory.Parse(template, defaults, constraints: null), new RouteValueDictionary(), order, metadata, @@ -149,8 +147,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing { return new MatcherEndpoint( next => c => Task.CompletedTask, - template, - defaults, + RoutePatternFactory.Parse(template, defaults, constraints: null), new RouteValueDictionary(), 0, EndpointMetadataCollection.Empty, diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj index ac7be75576..1e7cb2f215 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj @@ -50,6 +50,14 @@ + + + + + diff --git a/test/WebSites/BasicWebSite/BasicWebSite.csproj b/test/WebSites/BasicWebSite/BasicWebSite.csproj index 55879651a0..b71fbe1efc 100644 --- a/test/WebSites/BasicWebSite/BasicWebSite.csproj +++ b/test/WebSites/BasicWebSite/BasicWebSite.csproj @@ -10,6 +10,14 @@ + + + + +