简体   繁体   中英

Design pattern to implement DRY principle for Web API

I have the following two action methods in my controller. Both take the same parameter and do the same validation of the model. it differs at only one line where it makes a call to a service method.

Is there a better way to refactor this code?

[HttpPost]
public async Task<IActionResult> Search([FromBody]AggregateSearchCriteria criteria)
{
    if (criteria == null || !criteria.Aggregates.Any())
    {
        return BadRequest();
    }

    var providers = Request.Headers["providers"];

    if (providers.Equals(StringValues.Empty))
        return BadRequest();

    criteria.Providers = providers.ToString().Split(',').ToList();

    ModelState.Clear();

    TryValidateModel(criteria);

    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var result = await _searchService.Search(criteria);

    return Ok(result);
}

[HttpPost("rulebreak")]
public async Task<IActionResult> SearchRuleBreak([FromBody]AggregateSearchCriteria criteria)
{
    if (criteria == null || !criteria.Aggregates.Any())
    {
        return BadRequest();
    }

    var providers = Request.Headers["providers"];

    if (providers.Equals(StringValues.Empty))
        return BadRequest();

    criteria.Providers = providers.ToString().Split(',').ToList();

    ModelState.Clear();

    TryValidateModel(criteria);

    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var result = await _searchService.SearchRuleBreak(criteria);

    return Ok(result);
}

Something like this might be a start.

[HttpPost]
public async Task<IActionResult> Search([FromBody]AggregateSearchCriteria criteria)
{
    return await Common(criteria, c => _searchService.Search(c));
}

public async Task<IActionResult> SearchRuleBreak([FromBody]AggregateSearchCriteria criteria)
{
    return await Common(criteria, c => _searchService.SearchRuleBreak(c));
}

private async Task<IActionResult> Common(AggregateSearchCriteria criteria, Func<List<string>, Task<???>> action)
{
    if (criteria == null || !criteria.Aggregates.Any())
    {
        return BadRequest();
    }

    var providers = Request.Headers["providers"];

    if (providers.Equals(StringValues.Empty))
        return BadRequest();

    criteria.Providers = providers.ToString().Split(',').ToList();

    ModelState.Clear();

    TryValidateModel(criteria);

    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var result = await action.Invoke(criteria);

    return Ok(result);
}

Template pattern is the way to go in this kind of situations. Not related to the validation though. And you must take care of the dependency of controller too. Note that below code would not work automatically without some changes.

public abstract class BaseSearch
{
    public Task<IActionResult> Apply(AggregateSearchCriteria criteria)
    {
        if (criteria == null || !criteria.Aggregates.Any())
        {
            return BadRequest();
        }

        var providers = Request.Headers["providers"];

        if (providers.Equals(StringValues.Empty))
            return BadRequest();

        criteria.Providers = providers.ToString().Split(',').ToList();

        ModelState.Clear();

        TryValidateModel(criteria);

        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var result = await ServiceCall(criteria);

        return Ok(result);
    }

    protected abstract async IActionResult ServiceCall(AggregateSearchCriteria criteria);
}

public class Search : BaseSearch
{
    protected async Task<IActionResult> ServiceCall(AggregateSearchCriteria criteria)
    {
        return await _searchService.Search(criteria);
    }
}

public class SearchRuleBreak : BaseSearch
{
    protected async Task<IActionResult> ServiceCall(AggregateSearchCriteria criteria)
    {
        return await _searchService.SearchRuleBreak(criteria);
    }
}

Then while calling:

[HttpPost]
public async Task<IActionResult> Search([FromBody]AggregateSearchCriteria criteria)
{
    return await new Search().Apply(criteria);
}

[HttpPost("rulebreak")]
public async Task<IActionResult> SearchRuleBreak([FromBody]AggregateSearchCriteria criteria)
{
    return await new SearchRuleBreak().Apply(criteria);
}

Note: As the languages are getting more and more functional today, the "send function as parameter" approach is a valid way to go too, as @stuartd suggests.

You can implement IValidatableObject for your model AggregateSearchCriteria and move all validation logic inside it. For providers, you can add it in your model and write a custom data binder which will bind values from headers also you can write a coma array binder which will split your values into an array.

public class AggregateSearchCriteria : IValidatableObject
{
    [FromHeader]
    public IList<string> Providers { get; set; } = new List<string>();

    public IList<string> Aggregates { get; set; } = new List<string>();
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var result = new List<ValidationResult>();
        if (!Providers.Any())
        {
            result.Add(new ValidationResult("No Providers", new[] { nameof(AggregateSearchCriteria.Providers) }));
        }
        if (!Aggregates.Any())
        {
            result.Add(new ValidationResult("No Aggregates", new[] { nameof(AggregateSearchCriteria.Aggregates) }));
        }
        return result;
      }
   }

    [HttpPost]
    public async Task<IActionResult> Search([FromBody]AggregateSearchCriteria criteria)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var result = await _searchService.Search(criteria);

        return Ok(result);
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM