Where to set restrictions for objects when separating the "Business" level from the data level

I am trying to create business and data layers for my large ASP.NET MVC application. Since this is the first time that I am trying to complete a project of this magnitude, I read several books and try to carefully understand what needs to be done. Typically, my applications mix levels of business logic and data access, and several business objects are intertwined in one class (which confused me several times when I tried to figure out where to add things).

Most of what I read is the separation of business and data layers. Everything seems fine and dandy, but it's hard for me to figure out how to do this in some scenarios. For example, let's say I create a system that allows administrators to add a new product to the system:

public class Product { public int Id { get; private set; } public string Name { get; set; } public decimal Price { get; set; } } 

I then allocate data access by creating a repository

 public class ProductRepository { public bool Add(Product product); } 

Say I want the product name to contain at least 4 characters. I don’t understand how to do this.

One of my ideas was to extend the Name set property and set it only if it contains 4 characters. However, there is no way for the method that creates the product to know that the name is not set, except that Product.Name! = Everything that they passed.

Another idea I put is to put it in the Add () method in the repository, but then I have my business logic with data logic, which also means that if the Add failed failed call, I don’t know failed for business logic or because the DAL failed (and this also means that I cannot test it using mock frameworks).

The only thing I can think of is to put my DAL stuff in level 3, which is called from the Add () method in the repository, but I do not see this in any of the domain modeling examples in my book or on the Internet (as I saw , at least). It also adds complexity to domain models when I'm not sure if this is necessary.

Another example is the desire to make sure that a name is used by only one product. Will this go in the Product class, ProductRepository Add (), or where?

As a side note, I plan to use NHibernate as my ORM, however, to accomplish what I want (theoretically), it doesn't matter which ORM I use, since TDD should be able to isolate all this.

Thanks in advance!

+4
source share
8 answers

I usually approach this using a layered architecture. How to do it? Basically you have the following (ideally) VS projects:

  • Presentation level (where the user interface material is located)
  • Business layer (where the real business logic is located)
  • Data access level (where do you communicate with your base DBMS)

To decouple all of them, I use the so-called interface layers st in the end I

  • Presentation level (where the user interface is located)
  • IBusiness layer (containing interfaces for the business layer)
  • Business layer (where is the actual business logic)
  • IDataAccess layer (containing interfaces for the DAO layer)
  • The level of access to data (where you communicate with your base DBMS).

It is very convenient and creates a beautifully decoupled architecture. Basically, your presentation layer simply accesses the interfaces, not the implementations themselves. To create the appropriate instances, you should use Factory or preferably some dependency injection library ( Unity is suitable for .Net applications or, alternatively, Spring.Net).

How does this affect your business logic / testability of your application?
It is probably too long to write everything in detail, but if you are concerned about having a well-tested design, you should absolutely consider dependency injection libraries.

Using NHibernate, ... regardless of ORM
Having a DAO layer completely separated through interfaces from other layers, you can use any technology to access your base database. You can directly invoke SQL queries or use NHibernate as you wish. It's nice that it is completely independent of the rest of your application. You can start the event today by writing SQL files manually, and then exchange the DAO dll for the one that uses NHibernate without a single change in your BL or presentation level.
In addition, testing your BL logic is easy. You may have a class:

 public class ProductsBl : IProductsBL { //this gets injected by some framework public IProductsDao ProductsDao { get; set; } public void SaveProduct(Product product) { //do validation against the product object and react appropriately ... //persist it down if valid ProductsDao.PersistProduct(product); } ... } 

Now you can easily test the validation logic in your SaveProduct(...) method by selecting ProductDao in the test case.

+4
source

Put things like restricting the product name in the domain object, Product , if you do not want to allow products with less than 4 characters in some scenarios (in this case, you must apply the 4-character rule at the controller and / or client side). Remember that your domain objects can be reused by other controllers, actions, internal methods, or even other applications if you share the library. Your validation must match the abstraction you are modeling, regardless of application or use.

Since you use ASP.NET MVC, you should use the rich and widely extensible validation APIs included in the structure (search with the IDataErrorInfo MVC Validation Application Block DataAnnotations for more). There are many ways for the calling method to know that your domain object has rejected the argument - for example, by throwing an ArgumentOutOfRangeException .

Using the example of ensuring the uniqueness of product names, you would absolutely not put this in the Product class, because this requires knowledge of all other Product s. This logically refers to the save level and possibly to the repository. Depending on your use case, you may need a separate service method that checks that the name does not exist yet, but you should not assume that it will still be unique if you try to save it later (you need to check it again, because if you check the uniqueness and then save it longer than you save, someone else can save the record with the same name).

+2
source

So I do it:

I save the verification code in an entity class that inherits some common element interface.

 Interface Item { bool Validate(); } 

Then in the repository of the CRUD function, I call the corresponding validation function.

Thus, all logical paths check my values, but I need to look only in one place to see what this check is.

Plus, sometimes you use objects outside the repository area, for example, in a view. Therefore, if validation is split, each action path can test validation without requesting a repository.

+1
source

For limitations, I use partial classes in the DAL and implement data annotation validators. Quite often this is related to the creation of custom validators, but this works great because it is completely flexible. I was able to create very complex dependent checks that even got into the database as part of their reality checks.

http://www.asp.net/(S(ywiyuluxr3qb2dfva1z5lgeg))/learn/mvc/tutorial-39-cs.aspx

0
source

According to SRP (single responsibility principle), you might be better off if validation is separate from the product domain logic. Since this is required for data integrity, it probably should be closer to the repository - you just want to make sure that the validation is always performed without the need to think.

In this case, you may have a common interface (for example, IValidationProvider<T> ) that connects to a specific implementation through the IoC container or whatever your preference.

 public abstract Repository<T> { IValidationProvider<T> _validationProvider; public ValidationResult Validate( T entity ) { return _validationProvider.Validate( entity ); } } 

This way you can check your verification separately.

Your repository might look like this:

 public ProductRepository : Repository<Product> { // ... public RepositoryActionResult Add( Product p ) { var result = RepositoryResult.Success; if( Validate( p ) == ValidationResult.Success ) { // Do add.. return RepositoryActionResult.Success; } return RepositoryActionResult.Failure; } } 

You can take another step if you intend to expose this function through an external API and add a service level for mediation between domain objects and data access. In this case, you move the check to the service level and delegate access to the data in the repository. Perhaps you have IProductService.Add( p ) . But it can be a pain to maintain because of all the thin layers.

My $ 0.02.

0
source

Another way to achieve this through free communication is to create validator classes for your entity types and register them in your IoC, for example:

 public interface ValidatorFor<EntityType> { IEnumerable<IDataErrorInfo> errors { get; } bool IsValid(EntityType entity); } public class ProductValidator : ValidatorFor<Product> { List<IDataErrorInfo> _errors; public IEnumerable<IDataErrorInfo> errors { get { foreach(IDataErrorInfo error in _errors) yield return error; } } void AddError(IDataErrorInfo error) { _errors.Add(error); } public ProductValidator() { _errors = new List<IDataErrorInfo>(); } public bool IsValid(Product entity) { // validate that the name is at least 4 characters; // if so, return true; // if not, add the error with AddError() and return false } } 

Now that it's time for verification, ask the IoC for ValidatorFor<Product> and call IsValid() .

What happens if you need to change the validation logic? Well, you can create a new implementation of ValidatorFor<Product> and register it in your IoC instead of the old one. However, if you add another criterion, you can use a decorator:

 public class ProductNameMaxLengthValidatorDecorator : ValidatorFor<Person> { List<IDataErrorInfo> _errors; public IEnumerable<IDataErrorInfo> errors { get { foreach(IDataErrorInfo error in _errors) yield return error; } } void AddError(IDataErrorInfo error) { if(!_errors.Contains(error)) _errors.Add(error); } ValidatorFor<Person> _inner; public ProductNameMaxLengthValidatorDecorator(ValidatorFor<Person> validator) { _errors = new List<IDataErrorInfo>(); _inner = validator; } bool ExceedsMaxLength() { // validate that the name doesn't exceed the max length; // if it does, return false } public bool IsValid(Product entity) { var inner_is_valid = _inner.IsValid(); var inner_errors = _inner.errors; if(inner_errors.Count() > 0) { foreach(var error in inner_errors) AddError(error); } bool this_is_valid = ExceedsMaxLength(); if(!this_is_valid) { // add the appropriate error using AddError() } return inner_is_valid && this_is_valid; } } 

Update your IoC configuration, and now you have the minimum and maximum verification length without opening any classes for modification. This way you can link any number of decorators.

Alternatively, you can create many ValidatorFor<Product> implementations for various properties, and then ask IoC for all such implementations and run them in a loop.

0
source

Well, here is my third answer, because there are so many ways to trick this cat:

 public class Product { ... // normal Product stuff IList<Action<string, Predicate<StaffInfoViewModel>>> _validations; IList<string> _errors; // make sure to initialize IEnumerable<string> Errors { get; } public void AddValidation(Predicate<Product> test, string message) { _validations.Add( (message,test) => { if(!test(this)) _errors.Add(message); }; } public bool IsValid() { foreach(var validation in _validations) { validation(); } return _errors.Count() == 0; } } 

With this implementation, you can add an arbitrary number of validators to an object without hard-coding logic into a domain object. You really need to use an IoC or at least a basic factory to make sense.

Using:

 var product = new Product(); product.AddValidation(p => p.Name.Length >= 4 && p.Name.Length <=20, "Name must be between 4 and 20 characters."); product.AddValidation(p => !p.Name.Contains("widget"), "Name must not include the word 'widget'."); product.AddValidation(p => p.Price < 0, "Price must be nonnegative."); product.AddValidation(p => p.Price > 1, "This is a dollar store, for crying out loud!"); 
0
source

U may use a different verification system. you can add a method to IService in the service level, for example:

 IEnumerable<IIssue> Validate(T entity) { if(entity.Id == null) yield return new Issue("error message"); } 
0
source

Source: https://habr.com/ru/post/1300105/


All Articles