Using Razor Components in an ASP.NET Core MVC Application

Author Lee Timmins Date 2nd Oct 2025 Comments Comments

If you like the idea of Razor components but already have an existing ASP.NET Core MVC app, you can return a Razor component from within your MVC actions.

In this post, we’ll build a contact page with both server-side and client-side validation.

Step 1: Add a GET Action for the Contact Page

First, add a simple action in your controller to return the Razor component:

public IResult Contact()
{
    return new RazorComponentResult<Contact>();
}

Make sure the blazor-enhanced-nav header is not added for any actions returning RazorComponentResult. Otherwise, you’ll see the error: "An item with the same key has already been added. Key: blazor-enhanced-nav".

Step 2: Create the Contact Razor Component

Add a new Contact.razor file in the Views/Home directory:

@using Microsoft.AspNetCore.Components.Forms
@using System.ComponentModel.DataAnnotations

@if (!Submitted) {
    <EditForm FormName="ContactForm" Model="Model" OnValidSubmit="Submit" Enhance>
        <DataAnnotationsValidator />
        <ValidationSummary />
        <InputText class="form-control" @bind-Value="Model.Name" />
        <ValidationMessage For="() => Model.Name" />
        <button class="btn btn-primary">Submit</button>
    </EditForm>
} else {
    <p>Form submitted by @Model.Name!</p>
}

@code {
    [SupplyParameterFromForm]
    public ContactModel Model { get; set; } = new();

    [Parameter]
    public bool Submitted { get; set; }

    private void Submit() => Submitted = true;

    public class ContactModel {
        [Required]
        public string Name { get; set; } = default!;
    }
}

If this was a Blazor app then you would be done, however you need to make the followings mods to get this to work in your MVC app.

Step 3: Add a POST Action

Create a POST action to handle form submission:

[HttpPost("contact")]
public IResult Contact(ContactModel model)
{
    return new RazorComponentResult<Contact>(new { Model = model, Submitted = true });
}

Also, remove the OnValidSubmit="Submit" attribute and the Submit method from the Razor component.

You'll now see this error: "The property 'Model' on component type 'WebApplication1.Views.Home.Contact' cannot be set explicitly because it only accepts cascading values.". To fix this, replace [SupplyParameterFromForm] with [Parameter] in the Razor component to allow the POST action to set the model.

Step 4: Add Server-Side Validation

Update the component to handle server-side validation:

@using Microsoft.AspNetCore.Components.Forms
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Mvc.ModelBinding

@if (!Submitted || !ModelState.IsValid) {
    <EditForm FormName="ContactForm" EditContext="_editContext" Enhance>
        <DataAnnotationsValidator />
        <ValidationSummary />
        <InputText class="form-control" @bind-Value="Model.Name" />
        <ValidationMessage For="() => Model.Name" />
        <button class="btn btn-primary">Submit</button>
    </EditForm>
} else {
    <p>Form submitted by @Model.Name!</p>
}

@code {
    private EditContext? _editContext;

    [Parameter]
    public ContactModel Model { get; set; } = new();

    [Parameter]
    public ModelStateDictionary ModelState { get; set; } = new();

    [Parameter]
    public bool Submitted { get; set; }

    protected override void OnInitialized() {
        _editContext = new EditContext(Model);
        var messages = new ValidationMessageStore(_editContext);

        foreach (var error in ModelState.SelectMany(s => s.Value?.Errors.Select(e => (s.Key, e.ErrorMessage)) ?? [])) {
            messages.Add(_editContext.Field(error.Key), error.ErrorMessage);
        }
    }

    public class ContactModel {
        [Required]
        public string Name { get; set; } = default!;
    }
}

This manually defines the edit context, as you need to pass it into the ValidationMessageStore. I've also added a ModelState property to store the validation errors.

Update your POST action to populate ModelState:

[HttpPost]
public IResult Contact(ContactModel model)
{
    if (!ModelState.IsValid)
        return new RazorComponentResult<Contact>(new { Model = model, Submitted = true, ModelState });

    return new RazorComponentResult<Contact>(new { Model = model, Submitted = true });
}

Step 5: Add Client-Side Validation Attributes

My first idea was to look at how MVC handles this. For reference, see the AddAndTrackValidationAttributes method in the DefaultHtmlGenerator.cs file. Basically you need to call ValidationAttributeProvider.AddAndTrackValidationAttributes.

First inject the following services in your Razor component:

@inject IHttpContextAccessor HttpContextAccessor
@inject IModelMetadataProvider MetadataProvider
@inject ValidationHtmlAttributeProvider ValidationAttributeProvider

Make sure you've added the http context accessor to the services, for example: builder.Services.AddHttpContextAccessor().

Now add the following helper method:

public class FakeView : Microsoft.AspNetCore.Mvc.ViewEngines.IView {
    /// 
    public Task RenderAsync(ViewContext context) {
        return Task.CompletedTask;
    }

    /// 
    public string Path { get; } = "View";
}

private IDictionary GetCustomAttributes(object model, string propertyName) {
    var result = new Dictionary();

    var modelExplorer = MetadataProvider.GetModelExplorerForType(model.GetType(), model).GetExplorerForProperty(propertyName);
    var httpContext = HttpContextAccessor.HttpContext!;

    var tempDataProvider = httpContext.RequestServices.GetRequiredService();
    var viewContext = new ViewContext(
        new ActionContext(httpContext, httpContext.GetRouteData(), new ControllerActionDescriptor()),
        new FakeView(),
        new ViewDataDictionary(MetadataProvider, new ModelStateDictionary()),
        new TempDataDictionary(httpContext, tempDataProvider),
        TextWriter.Null,
        new HtmlHelperOptions()
    );

    ValidationAttributeProvider.AddAndTrackValidationAttributes(viewContext, modelExplorer, "", result);

    return result.ToDictionary(k => k.Key, k => (object)k.Value);
}

Finally apply the attributes to your input element:

<InputText class="form-control" @bind-Value="Model.Name" @attributes="GetCustomAttributes(Model, nameof(Model.Name))" />

Be sure to include the client-side validation libraries, as the Razor component will not automatically use your MVC layout page. If you want to load them globally, you can add @layout AppLayout at the top of your component to apply an AppLayout Razor component layout page.

I still find this code abit clunky. Therefore in the final code below I have replaced the GetCustomAttributes method with a different approach. I've also removed the DataAnnotationsValidator component, as it's not used.

Step 6: Final Razor Component

Here’s the completed Contact.razor with full server-side and client-side validation:

@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Mvc
@using Microsoft.AspNetCore.Mvc.Controllers
@using Microsoft.AspNetCore.Mvc.ModelBinding
@using Microsoft.AspNetCore.Mvc.ModelBinding.Validation
@using Microsoft.Extensions.Options
@using System.ComponentModel.DataAnnotations

@inject ClientValidatorCache ClientValidatorCache
@inject IHttpContextAccessor HttpContextAccessor
@inject IModelMetadataProvider MetadataProvider
@inject IOptions<MvcViewOptions> OptionsAccessor

@if (!Submitted || !ModelState.IsValid) {
    <EditForm FormName="ContactForm" EditContext="_editContext" Enhance>
        <ValidationSummary />
        <InputText class="form-control" @bind-Value="Model.Name" @attributes="GetCustomAttributes(Model, nameof(Model.Name))" />
        <ValidationMessage For="() => Model.Name" />
        <button class="btn btn-primary">Submit</button>
    </EditForm>
} else {
    <p>Form submitted by @Model.Name!</p>
}

@code {
    private EditContext? _editContext;

    [Parameter]
    public ContactModel Model { get; set; } = new();

    [Parameter]
    public ModelStateDictionary ModelState { get; set; } = new();

    [Parameter]
    public bool Submitted { get; set; }

    protected override void OnInitialized() {
        _editContext = new EditContext(Model);
        var messages = new ValidationMessageStore(_editContext);

        foreach (var error in ModelState.SelectMany(s => s.Value?.Errors.Select(e => (s.Key, e.ErrorMessage)) ?? [])) {
            messages.Add(_editContext.Field(error.Key), error.ErrorMessage);
        }
    }

    private IDictionary<string, object> GetCustomAttributes(object model, string propertyName) {
        var result = new Dictionary<string, string>();
        var metadata = MetadataProvider.GetMetadataForProperty(model.GetType(), propertyName);
        var clientValidatorProviders = OptionsAccessor.Value.ClientModelValidatorProviders;
        var clientModelValidatorProvider = new CompositeClientModelValidatorProvider(clientValidatorProviders);
        var validators = ClientValidatorCache.GetValidators(metadata, clientModelValidatorProvider);
        var httpContext = HttpContextAccessor.HttpContext!;
        var actionContext = new ActionContext(httpContext, httpContext.GetRouteData(), new ControllerActionDescriptor());
        var validationContext = new ClientModelValidationContext(actionContext, metadata, MetadataProvider, result);

        foreach (var validator in validators) {
            validator.AddValidation(validationContext);
        }

        return result.ToDictionary(k => k.Key, k => (object)k.Value);
    }

    public class ContactModel {
        [Required]
        public string Name { get; set; } = default!;
    }
}

Conclusion

By returning Razor components from MVC actions, you can seamlessly bring Razor components into your existing application while continuing to use MVC features like ModelState and client-side validation. This gives you the best of both worlds. You can also return Razor components that support streaming rendering (such as the Weather component) to take advantage of streaming within your MVC app.