File: ViewComponentResultTest.cs
Web Access
Project: src\src\Mvc\Mvc.ViewFeatures\test\Microsoft.AspNetCore.Mvc.ViewFeatures.Test.csproj (Microsoft.AspNetCore.Mvc.ViewFeatures.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.WebEncoders.Testing;
using Microsoft.Net.Http.Headers;
using Moq;
 
namespace Microsoft.AspNetCore.Mvc;
 
public class ViewComponentResultTest
{
    private readonly ITempDataDictionary _tempDataDictionary =
        new TempDataDictionary(new DefaultHttpContext(), Mock.Of<ITempDataProvider>());
 
    [Fact]
    public void Model_ExposesViewDataModel()
    {
        // Arrange
        var customModel = new object();
        var viewResult = new ViewComponentResult
        {
            ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider())
            {
                Model = customModel
            },
        };
 
        // Act & Assert
        Assert.Same(customModel, viewResult.Model);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_Throws_IfServicesNotRegistered()
    {
        // Arrange
        var actionContext = new ActionContext(new DefaultHttpContext() { RequestServices = Mock.Of<IServiceProvider>(), }, new RouteData(), new ActionDescriptor());
        var expected =
            "Unable to find the required services. Please add all the required services by calling " +
            $"'IServiceCollection.AddControllersWithViews()' inside the call to 'ConfigureServices(...)' " +
            "in the application startup code.";
 
        var viewResult = new ViewComponentResult();
 
        // Act
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => viewResult.ExecuteResultAsync(actionContext));
 
        // Assert
        Assert.Equal(expected, ex.Message);
    }
 
    [Fact]
    public async Task ExecuteAsync_ViewComponentResult_AllowsNullViewDataAndTempData()
    {
        // Arrange
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var actionContext = CreateActionContext(descriptor);
 
        var viewComponentResult = new ViewComponentResult
        {
            Arguments = new { name = "World!" },
            ViewData = null,
            TempData = null,
            ViewComponentName = "Text"
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
        // No assert, just confirm it didn't throw
    }
 
    [Fact]
    public async Task ExecuteResultAsync_Throws_IfNameOrTypeIsNotSet()
    {
        // Arrange
        var expected =
            "Either the 'ViewComponentName' or 'ViewComponentType' " +
            "property must be set in order to invoke a view component.";
 
        var actionContext = CreateActionContext();
 
        var viewComponentResult = new ViewComponentResult
        {
            TempData = _tempDataDictionary,
        };
 
        // Act and Assert
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(
            () => viewComponentResult.ExecuteResultAsync(actionContext));
        Assert.Equal(expected, exception.Message);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_Throws_IfViewComponentCouldNotBeFound_ByName()
    {
        // Arrange
        var expected = "A view component named 'Text' could not be found. A view component must be " +
            "a public non-abstract class, not contain any generic parameters, and either be decorated " +
            "with 'ViewComponentAttribute' or have a class name ending with the 'ViewComponent' suffix. " +
            "A view component must not be decorated with 'NonViewComponentAttribute'.";
 
        var actionContext = CreateActionContext();
 
        var viewComponentResult = new ViewComponentResult
        {
            ViewComponentName = "Text",
            TempData = _tempDataDictionary,
        };
 
        // Act and Assert
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(
            () => viewComponentResult.ExecuteResultAsync(actionContext));
        Assert.Equal(expected, exception.Message);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_Throws_IfViewComponentCouldNotBeFound_ByType()
    {
        // Arrange
        var expected = $"A view component named '{typeof(TextViewComponent).FullName}' could not be found. " +
            "A view component must be a public non-abstract class, not contain any generic parameters, and either be decorated " +
            "with 'ViewComponentAttribute' or have a class name ending with the 'ViewComponent' suffix. " +
            "A view component must not be decorated with 'NonViewComponentAttribute'.";
 
        var actionContext = CreateActionContext();
        var services = CreateServices(diagnosticListener: null, context: actionContext.HttpContext);
        services.AddSingleton<IViewComponentSelector>();
 
        var viewComponentResult = new ViewComponentResult
        {
            ViewComponentType = typeof(TextViewComponent),
            TempData = _tempDataDictionary,
        };
 
        // Act and Assert
        var exception = await Assert.ThrowsAsync<InvalidOperationException>(
            () => viewComponentResult.ExecuteResultAsync(actionContext));
        Assert.Equal(expected, exception.Message);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_ExecutesSyncViewComponent()
    {
        // Arrange
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var actionContext = CreateActionContext(descriptor);
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentName = "Text",
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        var body = ReadBody(actionContext.HttpContext.Response);
        Assert.Equal("Hello, World!", body);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_UsesDictionaryArguments()
    {
        // Arrange
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var actionContext = CreateActionContext(descriptor);
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new Dictionary<string, object> { ["name"] = "World!" },
            ViewComponentName = "Text",
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        var body = ReadBody(actionContext.HttpContext.Response);
        Assert.Equal("Hello, World!", body);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_ExecutesAsyncViewComponent()
    {
        // Arrange
        var methodInfo = typeof(AsyncTextViewComponent).GetMethod(nameof(AsyncTextViewComponent.InvokeAsync));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.AsyncText",
            ShortName = "AsyncText",
            TypeInfo = typeof(AsyncTextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var actionContext = CreateActionContext(descriptor);
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentName = "AsyncText",
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        var body = ReadBody(actionContext.HttpContext.Response);
        Assert.Equal("Hello-Async, World!", body);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_ExecutesViewComponent_AndWritesDiagnosticListener()
    {
        // Arrange
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var adapter = new TestDiagnosticListener();
 
        var actionContext = CreateActionContext(adapter, descriptor);
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentName = "Text",
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        var body = ReadBody(actionContext.HttpContext.Response);
        Assert.Equal("Hello, World!", body);
 
        Assert.NotNull(adapter.BeforeViewComponent?.ActionDescriptor);
        Assert.NotNull(adapter.BeforeViewComponent?.ViewComponentContext);
        Assert.NotNull(adapter.BeforeViewComponent?.ViewComponent);
        Assert.NotNull(adapter.AfterViewComponent?.ActionDescriptor);
        Assert.NotNull(adapter.AfterViewComponent?.ViewComponentContext);
        Assert.NotNull(adapter.AfterViewComponent?.ViewComponentResult);
        Assert.NotNull(adapter.AfterViewComponent?.ViewComponent);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_ExecutesViewComponent_ByShortName()
    {
        // Arrange
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var actionContext = CreateActionContext(descriptor);
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentName = "Text",
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        var body = ReadBody(actionContext.HttpContext.Response);
        Assert.Equal("Hello, World!", body);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_ExecutesViewComponent_ByFullName()
    {
        // Arrange
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var actionContext = CreateActionContext(descriptor);
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentName = "Full.Name.Text",
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        var body = ReadBody(actionContext.HttpContext.Response);
        Assert.Equal("Hello, World!", body);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_ExecutesViewComponent_ByType()
    {
        // Arrange
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var actionContext = CreateActionContext(descriptor);
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentType = typeof(TextViewComponent),
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        var body = ReadBody(actionContext.HttpContext.Response);
        Assert.Equal("Hello, World!", body);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_WithCustomViewComponentHelper()
    {
        // Arrange
        var expected = "Hello from custom helper";
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
        var result = Task.FromResult<IHtmlContent>(new HtmlContentBuilder().AppendHtml(expected));
 
        var helper = Mock.Of<IViewComponentHelper>(h => h.InvokeAsync(It.IsAny<Type>(), It.IsAny<object>()) == result);
 
        var httpContext = new DefaultHttpContext();
        var services = CreateServices(diagnosticListener: null, httpContext, new[] { descriptor });
        services.AddSingleton<IViewComponentHelper>(helper);
 
        httpContext.RequestServices = services.BuildServiceProvider();
        httpContext.Response.Body = new MemoryStream();
 
        var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentType = typeof(TextViewComponent),
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        var body = ReadBody(actionContext.HttpContext.Response);
        Assert.Equal(expected, body);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_WithCustomViewComponentHelper_ForLargeText()
    {
        // Arrange
        var expected = new string('a', 64 * 1024 * 1024);
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
        var result = Task.FromResult<IHtmlContent>(new HtmlContentBuilder().AppendHtml(expected));
 
        var helper = Mock.Of<IViewComponentHelper>(h => h.InvokeAsync(It.IsAny<Type>(), It.IsAny<object>()) == result);
 
        var httpContext = new DefaultHttpContext();
        var services = CreateServices(diagnosticListener: null, httpContext, new[] { descriptor });
        services.AddSingleton<IViewComponentHelper>(helper);
 
        httpContext.RequestServices = services.BuildServiceProvider();
        httpContext.Response.Body = new MemoryStream();
 
        var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentType = typeof(TextViewComponent),
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        var body = ReadBody(actionContext.HttpContext.Response);
        Assert.Equal(expected, body);
    }
 
    [Fact]
    public async Task ExecuteResultAsync_SetsStatusCode()
    {
        // Arrange
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var actionContext = CreateActionContext(descriptor);
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentType = typeof(TextViewComponent),
            StatusCode = 404,
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        Assert.Equal(404, actionContext.HttpContext.Response.StatusCode);
    }
 
    public static TheoryData<string, string> ViewComponentResultContentTypeData
    {
        get
        {
            return new TheoryData<string, string>
                {
                    {
                        null,
                        "text/html; charset=utf-8"
                    },
                    {
                        "text/foo",
                        "text/foo"
                    },
                    {
                        "text/foo;p1=p1-value",
                        "text/foo; p1=p1-value"
                    },
                    {
                        new MediaTypeHeaderValue("text/foo") { Encoding = Encoding.ASCII }.ToString(),
                        "text/foo; charset=us-ascii"
                    }
                };
        }
    }
 
    [Theory]
    [MemberData(nameof(ViewComponentResultContentTypeData))]
    public async Task ViewComponentResult_SetsContentTypeHeader(
        string contentType,
        string expectedContentType)
    {
        // Arrange
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var actionContext = CreateActionContext(descriptor);
 
        var contentTypeBeforeViewResultExecution = contentType?.ToString();
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentName = "Text",
            ContentType = contentType,
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        var resultContentType = actionContext.HttpContext.Response.ContentType;
        MediaTypeAssert.Equal(expectedContentType, resultContentType);
 
        // Check if the original instance provided by the user has not changed.
        // Since we do not have access to the new instance created within the view executor,
        // check if at least the content is the same.
        var contentTypeAfterViewResultExecution = contentType?.ToString();
        MediaTypeAssert.Equal(contentTypeBeforeViewResultExecution, contentTypeAfterViewResultExecution);
    }
 
    [Fact]
    public async Task ViewComponentResult_SetsContentTypeHeader_OverrideResponseContentType()
    {
        // Arrange
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var actionContext = CreateActionContext(descriptor);
 
        var expectedContentType = "text/html; charset=utf-8";
        actionContext.HttpContext.Response.ContentType = "application/x-will-be-overridden";
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentName = "Text",
            ContentType = new MediaTypeHeaderValue("text/html") { Encoding = Encoding.UTF8 }.ToString(),
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        Assert.Equal(expectedContentType, actionContext.HttpContext.Response.ContentType);
    }
 
    [Theory]
    [InlineData("text/foo", "text/foo; charset=utf-8")]
    [InlineData("text/foo; p1=p1-value", "text/foo; p1=p1-value; charset=utf-8")]
    [InlineData("text/foo; p1=p1-value; charset=us-ascii", "text/foo; p1=p1-value; charset=us-ascii")]
    public async Task ViewComponentResult_NoContentTypeSet_PreservesResponseContentType(
        string responseContentType,
        string expectedContentType)
    {
        // Arrange
        var methodInfo = typeof(TextViewComponent).GetMethod(nameof(TextViewComponent.Invoke));
        var descriptor = new ViewComponentDescriptor()
        {
            FullName = "Full.Name.Text",
            ShortName = "Text",
            TypeInfo = typeof(TextViewComponent).GetTypeInfo(),
            MethodInfo = methodInfo,
            Parameters = methodInfo.GetParameters(),
        };
 
        var actionContext = CreateActionContext(descriptor);
 
        actionContext.HttpContext.Response.ContentType = expectedContentType;
 
        var viewComponentResult = new ViewComponentResult()
        {
            Arguments = new { name = "World!" },
            ViewComponentName = "Text",
            TempData = _tempDataDictionary,
        };
 
        // Act
        await viewComponentResult.ExecuteResultAsync(actionContext);
 
        // Assert
        Assert.Equal(expectedContentType, actionContext.HttpContext.Response.ContentType);
    }
 
    private IServiceCollection CreateServices(
        object diagnosticListener,
        HttpContext context,
        params ViewComponentDescriptor[] descriptors)
    {
        var httpContext = new DefaultHttpContext();
        var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
        if (diagnosticListener != null)
        {
            diagnosticSource.SubscribeWithAdapter(diagnosticListener);
        }
 
        var services = new ServiceCollection();
        services.AddSingleton<DiagnosticListener>(diagnosticSource);
        services.AddSingleton<ViewComponentInvokerCache>();
        services.AddSingleton(Options.Create(new MvcViewOptions()));
        services.AddTransient<IViewComponentHelper, DefaultViewComponentHelper>();
        services.AddSingleton<IViewComponentSelector, DefaultViewComponentSelector>();
        services.AddSingleton<IViewComponentDescriptorCollectionProvider, DefaultViewComponentDescriptorCollectionProvider>();
        services.AddSingleton<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>();
        services.AddSingleton<ITypeActivatorCache, TypeActivatorCache>();
        services.AddSingleton<IViewComponentActivator, DefaultViewComponentActivator>();
        services.AddSingleton<IViewComponentFactory, DefaultViewComponentFactory>();
        services.AddSingleton<IViewComponentDescriptorProvider>(new FixedSetViewComponentDescriptorProvider(descriptors));
        services.AddSingleton<IModelMetadataProvider, EmptyModelMetadataProvider>();
        services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
        services.AddSingleton<ITempDataDictionaryFactory, TempDataDictionaryFactory>();
        services.AddSingleton<ITempDataProvider, SessionStateTempDataProvider>();
        services.AddSingleton<TempDataSerializer, DefaultTempDataSerializer>();
        services.AddSingleton<HtmlEncoder, HtmlTestEncoder>();
        services.AddSingleton<IViewBufferScope, TestViewBufferScope>();
        services.AddSingleton<IActionResultExecutor<ViewComponentResult>, ViewComponentResultExecutor>();
        services.AddSingleton<IHttpResponseStreamWriterFactory, TestHttpResponseStreamWriterFactory>();
 
        return services;
    }
 
    private HttpContext CreateHttpContext(object diagnosticListener, params ViewComponentDescriptor[] descriptors)
    {
        var httpContext = new DefaultHttpContext();
        var services = CreateServices(diagnosticListener, httpContext, descriptors);
 
        httpContext.Response.Body = new MemoryStream();
        httpContext.RequestServices = services.BuildServiceProvider();
 
        return httpContext;
    }
 
    private ActionContext CreateActionContext(object diagnosticListener, params ViewComponentDescriptor[] descriptors)
    {
        return new ActionContext(CreateHttpContext(diagnosticListener, descriptors), new RouteData(), new ActionDescriptor());
    }
 
    private ActionContext CreateActionContext(params ViewComponentDescriptor[] descriptors)
    {
        return CreateActionContext(null, descriptors);
    }
 
    private class FixedSetViewComponentDescriptorProvider : IViewComponentDescriptorProvider
    {
        private readonly ViewComponentDescriptor[] _descriptors;
 
        public FixedSetViewComponentDescriptorProvider(params ViewComponentDescriptor[] descriptors)
        {
            _descriptors = descriptors ?? new ViewComponentDescriptor[0];
        }
 
        public IEnumerable<ViewComponentDescriptor> GetViewComponents()
        {
            return _descriptors;
        }
    }
 
    private static string ReadBody(HttpResponse response)
    {
        response.Body.Seek(0, SeekOrigin.Begin);
 
        using (var reader = new StreamReader(response.Body))
        {
            return reader.ReadToEnd();
        }
    }
 
    private class TextViewComponent : ViewComponent
    {
        public HtmlString Invoke(string name)
        {
            return new HtmlString("Hello, " + name);
        }
    }
 
    private class AsyncTextViewComponent : ViewComponent
    {
        public Task<HtmlString> InvokeAsync(string name)
        {
            return Task.FromResult(new HtmlString("Hello-Async, " + name));
        }
    }
}