How to Design Validation, Error Management, and Message Service in a Layered Architecture for a WPF Project?

fatih uyanık 160 Reputation points
2025-01-13T10:16:24.99+00:00

Hello
I am working on a WPF application using the MVVM pattern and performing validations through a centralized ValidationService that leverages FluentValidation. During the validation process, I need to check whether an ISBN already exists in the database, but I am unsure whether this check should be performed in the validator (within FluentValidation rules) or in the service layer. Additionally, I am looking for the best approach to display error messages to the user.

In this context:

• FluentValidation is typically used for model validation, but can database-related business logic checks be implemented within the validator, or should they strictly belong to the service layer?

• Should I create a MessageService and use it directly in the service layer for displaying error messages, or should I pass the errors from the service layer to the ViewModel and utilize the MessageService in the ViewModel?

• How can I transfer validation errors and business logic checks from the service layer to the ViewModel without violating the principles of layered architecture and separation of concerns?

Could you provide suggestions for the best architectural design and an effective way to display error messages to the user in this scenario?
Thanks.

Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,809 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
11,195 questions
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Hongrui Yu-MSFT 3,730 Reputation points Microsoft Vendor
    2025-01-14T06:08:19.34+00:00

    Hi, @fatih uyanık. Welcome to Microsoft Q&A. 

    It is recommended to put database-related business logic in the service layer.You could define a separate service for the table with the ISBN attribute. The service contains the query operation on the database, here is the operation to check whether the ISBN exists.

    Then load FluentValidation and the service defined above in ValidationService to perform FluentValidation model validation and ISBN verification operations.

    The error message could define a separate data structure as the return value of ValidationService.

    Get ValidationService through dependency injection in ViewModel and perform validation, get error information, and control the display of error information in ViewModel.

    For dependency injection, you could refer to Using Microsoft.Extensions.DependencyInjection

    Reference Projects

    Project Structure(Assume that the table with ISBN is People)

    Picture1

    Install CommunityToolkit.Mvvm, FluentValidation, Microsoft.Extensions.DependencyInjection through NuGet.

    People.cs

        public class People
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string ISBN { get; set; }
        }
    

    PeopleService.cs

        public interface PeopleService
        {
            public bool CheckISBN(People people);
        }
    

    PeopleServiceImpl.cs

        public class PeopleServiceImpl : PeopleService
        {
            public bool CheckISBN(People people)
            {
    			//TODO: Verify the database
                return false;
            }
        }
    

    ValidationService.cs

        public interface ValidationService
        {
            public ValidationResult ValidatePeople(People people);
        }
    

    ValidationServiceImpl.cs

        public class ValidationServiceImpl : ValidationService
        {
    		//Import two services through dependency injection for verification
            private readonly PeopleService _peopleService;
            private readonly IValidator<People> _validator;
            public ValidationServiceImpl(PeopleService peopleService, IValidator<People> validator)
            {
                _peopleService = peopleService;
                _validator = validator;
            }
    		//Centrally verify these two services
            public ValidationResult ValidatePeople(People people)
            {
                var message = "";
                var result1 = _peopleService.CheckISBN(people);
                if (!result1)
                {
                    message += "ISBN not found \n";
                }
                var result2 = _validator.Validate(people);        
                if (!result2.IsValid)
                {
                    foreach (var failure in result2.Errors)
                    { 
                        message += $"Property {failure.PropertyName} failed validation. Error was: {failure.ErrorMessage} \n"; 
                    } 
                }
    
                if(result1&&result2.IsValid)
                {
                    return ValidationResult.Success();
                }
                else
                {
                    return ValidationResult.Failure(message);
                }  
            }
        }
    

    PeopleValidator.cs

        public class PeopleValidator:AbstractValidator<People>
        {
            public PeopleValidator() { 
                RuleFor(people => people.Id)
                    .NotEmpty().WithMessage("Id cannot be empty");
    
                RuleFor(people => people.Name)
                    .NotEmpty().WithMessage("Name cannot be empty")
                    .MinimumLength(2).WithMessage("Minimum 2 characters for the name");
    
                RuleFor(people => people.ISBN)
                    .NotEmpty().WithMessage("ISBN cannot be empty");
            }
        }
    

    ValidationResult.cs(The data structure used for message return. You could also design it according to your preferences.)

        public class ValidationResult
        {
            public bool IsSuccess { get; set; }
    
            public string Message { get; set; }
    
            public static ValidationResult Success() => new ValidationResult { IsSuccess = true };
            public static ValidationResult Failure(string message) => new ValidationResult { IsSuccess = false,Message = message };
        }
    

    MainWindow.xaml

        <Grid>
            <Frame x:Name="MyPage"></Frame>
        </Grid>
    

    MainWindow.xaml.cs

        public partial class MainWindow : Window
        {
            public MainWindow(IServiceProvider serviceProvider)
            {
                InitializeComponent();
                var page1 = serviceProvider.GetRequiredService<Page1>();
                MyPage.Navigate(page1);      
            }
        }
    

    Page1.xaml

        <Grid>
            <Button Content="Click Me" Command="{Binding relayCommand}" Width="200" Height="100" ></Button>
        </Grid>
    

    Page1.xaml.cs

        public partial class Page1 : Page
        {
            public Page1(Page1ViewModel page1ViewModel)
            {
                InitializeComponent();      
                this.DataContext = page1ViewModel;
            }
        }
    

    Page1ViewModel.cs

        public class Page1ViewModel:ObservableObject
        {
    		//Obtaining services through dependency injection
            private readonly ValidationService _validationService;
    
            public RelayCommand relayCommand {  get; set; }
     
            public Page1ViewModel(ValidationService validationService)
            {
                relayCommand = new RelayCommand(Fun);
                _validationService = validationService;
            }
    		//Get the returned results and display them
            public void Fun()
            {
                var result = _validationService.ValidatePeople(new People());
                MessageBox.Show(result.Message); 
            }
        }
    

    App.xaml.cs(Configure dependency injection here)

        public partial class App : Application
        {
            public IServiceProvider _serviceProvider { get;private set; }
    
            public App()
            {
                var serviceCollection = new ServiceCollection();
                ConfigureServices(serviceCollection);
                _serviceProvider = serviceCollection.BuildServiceProvider();
            }
    
            protected override void OnStartup(StartupEventArgs e)
            {
                var mainWindow = _serviceProvider.GetRequiredService<MainWindow>(); 
                mainWindow.Show();
                base.OnStartup(e);
            }
    
            private void ConfigureServices(IServiceCollection services)
            {
                services.AddTransient<ValidationService,ValidationServiceImpl>();
    
                services.AddTransient<PeopleService, PeopleServiceImpl>();
    
                services.AddTransient<IValidator<People>, PeopleValidator>();
    
                services.AddTransient<Page1ViewModel>();
    
                services.AddTransient<Page1>();
    
                services.AddTransient<MainWindow>();
            }
        }
    

    Finally, you should delete StartupUri="MainWindow.xaml" in App.xaml to avoid repeated startup.


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.