Skip to content

aaart/pipesharp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

Introduction

During my recent work spent generally on developing RESTful APIs, I noticed that a REST call is built in kind od "stream" or "pipeline" way. It means:

  • some input is taken for processing
  • taken input is validated
  • if validation fails then API returns error, otherwise:
  • processing moves forward, sometimes fetches more data (like querying DB) and the process verifies if fetched data can be processed with the given input
  • processing is summarized/finalized, operation result is created and sometimes wrapped into a generic object (response)

To address this scenario, I created simple library that gives you a posibillity to process input like a pipeline.

First of all you will need to know base error type returned from your code:

public class GenericError // no specific base type required, can be struct
{
    public string Message { get; set; }
    public int SomeCode { get; set; }    
}

//....
{
    IFlowBuilder<TFilteringError> builder = 
        new StandardBuilder()
            .UseErrorType<GenericError>();
}

When you know what type of errors your code might return then you can do some tuning:

builder
    .OnChanging(() =>
        {
            // this lambda would be executed before any Apply() or Finalize() invocation
        }
    ).OnChanged(() =>
        {
            // this lambda would be executed after any Apply() or Finalize() invocation
        });

Below you can find a simple code sample that creates new Book entity, with a given title publish date and author (given as author id):

public class BookService
{
    private DBContext _context;
    public BookService(DBContext context)
    {
        _context = context;
    }

    public Result<Book> Create(string title, DateTime published, int authorId)
    {
        if (string.IsNullOrWhiteSpace(title))
        {
            throw new ArgumentException("nope");
        }
        if (published.Year < 2000)
        {
            throw new ArgumentException("I don't like old millennium");
        }
        if (_context.Set<Author>().Count(a => a.Id == authorId) == 0)
        {
            throw new KeyNotFoundException("Sorry, given author does not exist");
        }
        var newBook = new Book 
        {
            AuthorId = authorId,
            Title = title,
            Published = published
        };
        _context.Attach(newBook);
        _context.SaveChanges();
        return new Result { Value = newBook };
    }
}

With PipeSharp you can write the following code:

public class BookService
{
    private DBContext _context;
    private IFlowBuilder<TFilteringError> _builder;
    public BookService(DBContext context, IFlowBuilder<TFilteringError> builder)
    {
        _context = context;
        _builder = builder;
    }

    public Result<Book> Create(string title, DateTime published, int authorId)
    {
        var (res, _, _) = _builder
            .For((title, published, authorId))
            .Check(x => x.title, t => !string.IsNullOrWhiteSpace(t), () => new TitleError())
            .Check(x => x.published, p => t.Year >= 2000, () => new YearError())
            .Check(x => x.authorId,
                        aId => _context
                                    .Set<Author>()
                                    .Count(a => a.Id == aId) == 1, 
                        () => new NoAuthorError())
            .Finalize(x => 
            {
                var newBook = new Book 
                {
                    AuthorId = authorId,
                    Title = title,
                    Published = published
                };
                _context.Attach(newBook);
                _context.SaveChanges();
                return newBook;
            })
            .Project(v => new Result { Value = v })
            .Sink();
        return res.Value;
    }
}

OOTB Exception handling:

[Fact]
public void Throw_Catch()
{
    var (res, ex, _) = _builder
        .For("input")
        .Finalize(x => { throw new NotImplementedException(); return x; })
        .Sink();
    
    Assert.True(res.Failed);
    Assert.IsType<NotImplementedException>(ex);
}

When you check input or applied changes it does not mean you throw exception:

[Fact]
public void CheckFailed_ValidationErrorExpected()
{
    var (res, ex, errors) = _builder
        .For("input")
        .Apply(x => 10)
        .Check(x => x == 0, () => new NotZero())
        .Finalize(x => { throw new NotImplementedException(); return x; })
        .Sink();
    
    Assert.True(res.Failed);
    Assert.IsNull(ex);
    Assert.Single(errors);
}

You can notify 3rd party components that something happened (but specific integration you need to do on yourself - no integration with any libraries has been done so far)

// LatePublishEventReceiver will raise all events when Sink() is done
// ImmediatePublishEventReceiver will raise event when Raise() is called
class SampleLatepublishEventReceiver : LatePublishEventReceiver
{
    protected virtual Action CreatePublisher<TEvent>(TEvent e) => () => Console.WriteLine("Hello World!");
}

// ...

new StandardBuilder()
    .UseErrorType<GenericError>()
    .EnableEventSubscription(new GenericEventReceiverFactory<SampleLatepublishEventReceiver>())
    .For(default(int))
    .Raise(x => new TestingEvent())
    .Finalize(x => x)
    .Sink();

You can map Exception to your custom error type and deconstruct pipeline result to

var (result, errors)

instead of

var (result, exception, errors)

How to map Exception to Error:

public void MapExceptionToErrorExample()
{
    var (_, errors) = _builder
        .MapExceptionToErrorOnDeconstruct(ex => new GenericError { Message = ex.Message, SomeCode = ex.HResult })
        .For(0)
        .Finalize(x =>
        {
            throw new Exception();
            return x;
        })
        .Sink();
    Assert.Single(errors);
}