Skip to content

A database-agnostic symmetrical Repository Pattern implementation that allows you to create DI-friendly CRUD+Search respositories for entities

License

Notifications You must be signed in to change notification settings

SeanCPP/sdotcode-DataLib

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sdotcode-DataLib

A database-agnostic, write-once, run everywhere repository pattern implementation for an entire dotnet .solution. This isn't intended to replace any ORM or database libraries. This is a symmetrical repository pattern that allows you to handle data access in every dotnet application in your solution in a consistent, elegant way.

Since certain tasks (in regard to data acess) have become so trivialized by the abstractions brought into the .net ecosystem, we can start DRYing our code across the tech stack pretty seamlessly. The repository pattern can be mirrored between the back-end and the front-end, which provides you a seamless approach to dealing with boring CRUD operations across layers of the tech stack.

Mirrored Repository Pattern

Using this as a solution-wide repository layer

Model class

[TableName("People")]
public class PersonModel : IStoredItem
{
    public virtual int Id { get; set; }
    
    [Searchable]
    public virtual string? Name { get; set; } = string.Empty;
}

Service class

public class PersonService : Service<PersonModel>
{
    public PersonService(IDataStore<PersonModel> dataStore) : base(dataStore) { }
}

The PeopleController class (if using the API features)

[ApiController]
[Route("[controller]")]
public class PeopleController : ExtendedControllerBase<PersonModel>
{
   public PeopleController(Service<PersonModel> service) : base(service) { }
}

By subclassing the ExtendedControllerBase in your code, you get a fully-functioning CRUD+Search API for that model class generated automatically:

Auto-generated API example

With these classes set up and registered in the DI container, you could now utilize Service in any project in the solution and perform CRUD and Search operations in an API controller, Blazor component, Xamarin/MAUI app, etc.

Blazor component

@foreach(var person in people)
{
    <p>@person.Id - @person.Name</p>
}

<input type="text" @bind-value=nameSearch />
<button @onclick=Search>Search</button>

@code {
    [Inject] Service<PersonModel>? service { get; set; }

    private string nameSearch = string.Empty;

    IEnumerable<PersonModel> people = new List<PersonModel>();

    private async Task Search()
    {
        people = await service!.SearchAsync(nameSearch, 
            pagingOptions: default,
            x => x.Name, 
            x => x.Id); // This won't get searched since the Id property on PersonModel doesn't have [Searchable]
    }
}

The IDataStore interface

In order to make this work, we need to plug an IDataStore implemenation into the DI containers of each application. So far, there are two (2) built-in IDataStore implementations (more are coming): InMemoryDataStore and HttpClientDataStore

Note If you need an IDataStore for your database/data source, simply implement the IDataStore interface.

The InMemoryDataStore is an in-memory data store that can be used for mocking / testing.

The HttpClientDataStore is more interesting.

The HttpClientDataStore

When using the Service class with HttpClientDataStore registered as the IDataStore, the Service will make HTTP requests to the appropriate controller methods behind the scenes.

If you need to add role authorization to any controller method, you can always override the method in your controller and add the [Authorize] attribute:

[Authorize]
public override Task<ActionResult> Upsert([FromBody] IEnumerable<PersonModel> items) => base.Upsert(items);

Wiring up the DI

It's important to note that in order for the system to automatically wire up and communicate with the API, the HttpClient must be properly configured to point to the API project, and your API project must allow CORS from your front-end applcation.

The Blazor project's Program.cs

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://localhost:7051/") }); // URL to the API project

builder.Services.AddWebApiRepository<PersonModel, PersonService>();
// The above line is a shortcut for:
// builder.Services.AddScoped<IDataStore<PersonModel>, HttpClientDataStore<PersonModel>>();
// builder.Services.AddScoped<Service<PersonModel>, PersonService>();

The API Project's Program.cs

builder.Services.AddSingleton<IDataStore<PersonModel>, InMemoryDataStore<PersonModel>>();
builder.Services.AddSingleton<Service<PersonModel>, PersonServiceMock>();
  
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
builder.Services.AddCors(options =>
{
    options.AddPolicy(
        name: MyAllowSpecificOrigins,
        policy =>
        {
            policy.WithOrigins("https://localhost:7220"); // URL to the front-end application that makes requests to this API
        });
});

var app = builder.Build();

// Later on...
app.UseCors(MyAllowSpecificOrigins);

API Reference


Get Single By Id

Gets a single instance of T based on the id

Task<T> GetAsync
     int id

Example:

var item = await MyService.GetAsync(id);

Get All (paginated)

Gets a list of T items based on pagination details (get all)

Task<IEnumerable<T>> GetAsync
     PagingInfo? pagingOptions = null

Example:

    var items = await GetAsync(); // Get All
    var items = await GetAsync(new PagingInfo { Page = 0, PageSize = 25 }); // Paging options

Get By [property]

Gets a list of T items where the given property matches value

    Task<IEnumerable<T>> GetAsync
        Expression<Func<T, object?>> propertyExpr, 
        object value, 
        PagingInfo? pagingOptions = null

Example:

    var items = await GetAsync(x => x.Name, "Moe"); // Get Any PersonModel items with the Name "Moe"

Search

"Single-textbox" Search

Searches for a list of items where any of the given properties contain query Note This will only search properties that are marked with the [Searchable] attribute.

  Task<IEnumerable<T>> SearchAsync
      string query,
      PagingInfo? pagingOptions = null, 
      params Expression<Func<T, object>>[] propertiesToSearch

Example:

    var items = await SearchAsync("Moe", pagingOptions: default, x => x.Name, x => x.Id); // Searches Name and Id properties for "Moe"

"Multi-textbox" / Form Search

Searches for a list of items where each each property in SearchType is compared against the corresponding property in T Note This will only search properties that have [Searchable] attribute. If the Entity type T doesn't contain a property inside your Search Model, then the search on that property will be skipped and no error will be thrown.

    Task<IEnumerable<T>> SearchAsync<SearchType>
          SearchType searchModel, 
          PagingInfo? pagingOptions = null

Example:

class PersonSearchModel
{
    public string? Name { get; set; }
}

// Later...
<div class="form-group">
    <label for="formName">Name</label>
    <input id="formName" class="form-control" type="text" @bind-value=searchModel.Name />
</div>
<button class="btn btn-primary" @onclick=FormSearch>Search</button>

@code {
  [Inject] Service<PersonModel>? service { get; set; }

  private PersonSearchModel searchModel = new();

  IEnumerable<PersonModel> results = new List<PersonModel>();

  private async Task FormSearch()
  {
      results = await service!.SearchAsync(searchModel, pagingOptions: default);
  }
}

Add or Update Single

Adds a new record if the entity doesn't exist, otherwise it updates the existing record.

  public Task<T> UpsertAsync 
        T entity

Example:

    var item = await UpsertAsync(new PersonModel { Name = "Moe" });

Add or Update Multiple

Adds new records if the entities don't exist, otherwise it updates the existing records.

 public Task<IEnumerable<T>> UpsertAsync
     IEnumerable<T> items

Example:

    var items = await UpsertAsync(listOfItems);

Delete Single

Delete item by Id

  public Task<bool> DeleteAsync(int id)

Example:

    var deletedSuccessfully = await DeleteAsync(1);  


Additional notes

Error Handling

A side effect to the way this is designed is that debugging your data access layer while developing actually becomes much simpler. If you override the OnException(Exception ex) method in your Service class and leave a breakpoint inside it, you will automatically hit the stop if a data access error occurs in any application in your solution during runtime. This is equivilent to setting a breakpoint in every catch(){ } statement in your Entity's repository.

Furthermore, if you set a breakpoint inside the HandleException method of an IDataStore, you'll hit a stop if a data access error occurs in any application for every single Entity in your solution during runtime. (as long as it's using that IDataStore) which is, a lot of power for one breakpoint. And is in my opinion, overall pretty cool.

public class PersonService : Service<PersonModel>
{
    public PersonService(IDataStore<PersonModel> dataStore) : base(dataStore) { }

    protected override Task OnException(Exception ex)
    {
        return base.OnException(ex);
    }
}

Heads up

If you plan on using this as an out-of-the-box solution, I'd highly recommend forking the project instead of cloning it directly from here. This is very much so a work-in-progress, and it will likely change. This is not a stable, production-ready product yet, as ideas are being experimented with and improved upon. This project does not make any promises regarding security or stability of data access. You'll need to rely on applying security measures (auth, roles, etc) to the data access in API controllers and database operations as you normally would elsewhere.

This can become a stable production-ready product with the help of community contributions. If this seems like something you or your company could benefit from, feel free to get involved!

About

A database-agnostic symmetrical Repository Pattern implementation that allows you to create DI-friendly CRUD+Search respositories for entities

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published