Skip to content

Instantly share code, notes, and snippets.

@vmandic
Last active August 20, 2018 10:56
Show Gist options
  • Save vmandic/b17ca389fe6d26d58f06dfae1d75e891 to your computer and use it in GitHub Desktop.
Save vmandic/b17ca389fe6d26d58f06dfae1d75e891 to your computer and use it in GitHub Desktop.

An EF Core / OData - Generic Repository πŸ›  😡

This library offers an implementation of the Generic Repository Pattern for Microsoft's Entity Framework Core 2.0 ORM supporting the use of OData v4 querying service protocol.

The OData part of the library offers an additional Repository implementation named ODataEfRepository with its complementing interface type IODataEfRepository. The OData Repository exposes several methods that allow querying an IQueryable<T> with OData's query options where the queryable can be produced out of EF's DbSet<T> or any other valid IQueryable<T>. This way you can pre/post filter/select/expand EF queryable sources using OData syntax.

Beta dependency caution πŸ’£

The library currently uses the latest Microsoft.AspnetCore.OData (7.0.0-beta2) to provide OData services and it might be buggy as it is not a stable offical release.

Install via NuGet or .NET CLI πŸ“₯

NuGet:

PM> Install-Package EnumODataEfRepository

.NET CLI:

dotnet add package EnumODataEfRepository

How to use πŸ’…

Assuming we are using a .NET standard 2.0 application, we should do couple of things:

  1. (optional config step, you can skip it) Create an "ODataEfRepository" configuration object inside your appsettings.json (you can override these configs from code also):
"ODataEfRepository": {
    "PageSize": 24,
    "CountPageTotals": true,

    // here specify the namespace of your EF/CodeFirst types for which Repositories will be injected
    "EntityTypesNamespaceToRegister": "YourApp.DataModels",

    "AutoRegisterScopedRepositories": true,
    "UseODataEfRepository": true,
    "UseOData": true
}

// All available properties are optional, so is the whole config...

You can read more about options in the Configuration options section.

  1. In your Startup.cs and its ConfigureServices method ensure you have added your DbContext like so:
services.AddDbContext<YourDbContext>();
  1. After adding the DbContext from step #2 you can add the ODataEfRepository service in the next line by calling the IServiceCollection extension method AddODataEfRepository(). You will need to provide an IConfiguration instance if you wish to read the configuration from your appsettings.json:
// first, provide a needed using directive:
using EnumODataEfRepository.Extensions;
  1. Now you can apply πŸ’‰ any of the following overloads to load the ODataEfRepository services:
// no extra config, using defaults:
services.AddODataEfRepository<YourDbContext>(config);

// reading from config:
services.AddODataEfRepository<YourDbContext>(config);

// providing base opts and patching from JSON config:
services.AddODataEfRepository<YourDbContext>(baseOptions, config);
  1. (this step is needed only if you are using OData) In the Configure method inject the EdmContainer and ODataEfRepositoryOptions, then call the IApplicationBuilder extension method to initialized MVC default routing and OData services:
public void Configure(IApplicationBuilder app, EdmContainer edmContainer, ODataEfRepositoryOptions opts)
{
    app.UseMvcWithDefaultRouteAndODataEfRepository(edmContainer, opts);
}
  1. Now depending on the options, if you selected to UseOData and UseODataEfRepository and if proper entity types were successfuly registerd you should be able to inject the OData variant repository like IODataEfRepository<YourEntity> or the EF only variant like IEfRepository<YourEntity> in you desired constructors. For example in an ASP.NET Core MVC app you would now do:
// CarsController.cs

private readonly IOdataEfRepository<Car> _carsRepo;

public CarsController(IODataEfRepository<Car> carsRepo)
{
    _carsRepo = carsRepo;
}

public IActionResult GetRedWithExtraFilterCars(string oDataFilterQuery, string oDataOrderByQuery)
{
    // prefilter some data with the base IEfREpository interface and its Filter method:
    var redCarsQuery = carsRepo.Filter(car => car.Color == "red", carsIncl => carsIncl.Include(car => car.FormerOwners));

    // use the ODataQuery and ODataSelectExpandQuery to construct OData queries:
    var oDataQuery = new ODataQuery(filter: oDataFitlerQuery, orderBy: oDataOrderByQuery);

    // use the ODataQuery objects to query against the OData datasource for the same entity:
    var result = carsRepo.GetODataQueryable(oDataQuery, applyToQuery: redCarsQuery).ToList();

    return result;
}

Configure options πŸ”§

The library offers configuration through JSON configuration from your appsettings.json or through code by using an options object, i.e. by providing a base options instance of ODataEfRepositoryOptions. When using both, the base options object comes first and the JSON config updates the base options to produce a final config.

Applying the combined configuration, by providing a base options object that will get additionally updated by values from the JSON config:

services.AddODataEfRepository<YourDbContext>(baseOptions, config);

List of all options with an explanation πŸ“‹

/// <summary>
/// (int) default: 24.
///
/// The defualt page size of a page that a Repository page method will return.
/// </summary>
public int PageSize { get; set; } = Consts.DEFAULT_PAGE_SIZE;

/// <summary>
/// (bool) default: true.
///
/// Determines if a total of records will be counted while retrieving a Repository page result.
/// Setting this option to false will improve query performance due to skipping the additional count call.
/// </summary>
public bool CountPageTotals { get; set; } = true;

/// <summary>
/// (string) default: null.
///
/// A FQN of a namespace available in your source code that can be used to pick Entity Framework and OData types.
/// The types are used to generate an OData <see cref="Microsoft.OData.Edm.IEdmModel"/> model and to register generic Repository instances.
/// </summary>
public string EntityTypesNamespaceToRegister { get; set; } = null;

/// <summary>
/// (enum) default: 0 (<see cref="QueryTrackingBehavior.TrackAll"/>).
///
/// The default behavior to set up how Entity Framework query tracking works.
/// Available options are 0 ('TrackAll' - tracks entities, useful for updating entities) and 1 ('NoTracking' - no tracking is applied, better performance).
///
/// Caution: The NoTracking used for partially updates is a bit buggy due to unstability of EFCore.
/// </summary>
public QueryTrackingBehavior EfQueryTracking { get; set; } = QueryTrackingBehavior.TrackAll;

/// <summary>
/// (bool) default: false.
///
/// Determines if the automatic service collection registration of the types supplied from the namespace set in the option <see cref="ODataEfRepositoryOptions.EntityTypesNamespaceToRegister"/> will be applied.
/// If the entity namespace is not set this option will have no effect.
/// This option is dependent on the option value <see cref="ODataEfRepositoryOptions.AutoRegisterScopedRepositories"/> which will determin the type of Repository that will be used.
/// </summary>
public bool AutoRegisterScopedRepositories { get; set; } = false;

/// <summary>
/// (bool) default: true.
///
/// Determines if the OData Repository feature of the library will be used.
/// The OData repository allows additional repository methods that enable OData query interpretation and combination with other IQueryable objects.
/// This option is dependent on the option value <see cref="ODataEfRepositoryOptions.UseOData"/> which controls if OData is used generally with the library.
/// </summary>
public bool UseODataEfRepository { get; set; } = true;

/// <summary>
/// (bool) default: true.
///
/// Determines if the OData service will be added which enables the usage of <see cref="Microsoft.AspNet.OData.ODataController"/> and other OData features.
/// This has to be set to 'true' if one wishes to use the <see cref="EnumODataEfRepository.Core.Providers.ODataEntityFramework.ODataEfRepository{TEntity}"/> as the Generic Repository provider while also setting the option <see cref="ODataEfRepositoryOptions.UseODataEfRepository"/> to 'true'.
/// </summary>
public bool UseOData { get; set; } = true;

/// <summary>
/// (string) default: 'odata'.
///
/// Determines the OData's default root route which can be used for querying the $metadata specification of the OData service.
/// The default option will set up the route "~/odata", which means that this will be the default prefix route for additional ODataControllers if any will be used.
/// An example route accessing an entity would be "~/odata/MyEntityType", and a query with a filter could be: "~/odata/MyEntityType?$filter=SomeIntColumn eq 123".
/// </summary>
public string ODataRoute { get; set; } = "odata";

/// <summary>
/// (bool) default: false.
///
/// Determines if a DbContext.SaveChanges() call will be done inside the Add, Updated and Delete methods of the <see cref="EnumODataEfRepository.Core.Providers.EntityFramework.IEfRepository{T}"/>.
/// </summary>
public bool SaveChangesImmediatelly { get; internal set; }

IEfRepository interface πŸ‘‘

// Entity fetch methods:
T GetFirstOrDefault(Expression<Func<T, bool>> searchPredicate = null, Func<IQueryable<T>, IIncludableQueryable<T, object>> include = null);
IEnumerable<T> GetAll(Expression<Func<T, bool>> searchPredicate = null, Func<IQueryable<T>, IIncludableQueryable<T, object>> include = null);


// Query methods:
IQueryable<T> QueryableAll(Func<IQueryable<T>, IIncludableQueryable<T, object>> include = null);
IQueryable<T> Filter(Expression<Func<T, bool>> searchPredicate, Func<IQueryable<T>, IIncludableQueryable<T, object>> include = null);


// Queryable pagination methods:
IOrderedQueryable<T> FilterSort<OrderingType>(Expression<Func<T, OrderingType>> orderBy, SortOrder sortOrder = SortOrder.Asc, Expression<Func<T, bool>> searchPredicate = null, Func<IQueryable<T>, IIncludableQueryable<T, object>> include = null);
(IQueryable<T> PageQuery, long TotalFiltered) PageFilter(IOrderedQueryable<T> orderedQuery, Expression<Func<T, bool>> searchPredicate = null, int? page = 1, int? size = null, Func<IQueryable<T>, IIncludableQueryable<T, object>> include = null);
(IQueryable<T> PageQuery, long TotalFiltered) FilterSortPage<OrderingType>(Expression<Func<T, OrderingType>> orderBy, SortOrder sortOrder = SortOrder.Asc, Expression<Func<T, bool>> searchPredicate = null, int? page = 1, int? size = null, Func<IQueryable<T>, IIncludableQueryable<T, object>> include = null);


// Addition methods:
T Add(T entity);
void Add(IEnumerable<T> entites);


// Update methods:
void Update(T entity);
void UpdateForProperties(T entity, params Expression<Func<T, object>>[] updatedProperties);


// Removal methods:
void Remove(params T[] entities);
void Remove(params object[] entityKeys);
void Remove(Expression<Func<T, bool>> searchPredicate);

// Utility methods:
T ReloadEntityFromDb(T entity);

IODataEfRepository interface πŸ‘ 

// Entity fetch methods:
TEntity GetODataFirstOrDefault(ODataQuery queryOptions, Expression<Func<TEntity, bool>> searchPredicate, Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> includeFunc = null, ODataQuerySettings querySettings = null);

IEnumerable<TEntity> GetODataAll(ODataQuery queryOptions, Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> includeFunc = null, ODataQuerySettings querySettings = null);

IQueryable<TEntity> GetODataQueryable(ODataQuery queryOptions, IQueryable<TEntity> applyToQuery = null, ODataQuerySettings querySettings = null);

IQueryable GetODataSelectExpand(ODataSelectExpandQuery queryOptions, IQueryable<TEntity> applyToQuery = null, ODataQuerySettings querySettings = null);

T GetODataSelectExpand<T>(ODataSelectExpandQuery queryOptions, IQueryable<TEntity> applyToQuery = null, ODataQuerySettings querySettings = null);


// Query methods:
IQueryable<TEntity> FilterOData(ODataQuery queryOptions, Expression<Func<TEntity, bool>> searchPredicate, Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> includeFunc = null, ODataQuerySettings querySettings = null);

Caution when using select/expand OData query πŸ‘€

Notice that IQueryable GetODataSelectExpand has a non-generic IQueryable return type. That is because the select and expand OData query parameters decide which values will be returned. You can use the other overload with generic support which will map to the desired type if it matches the return type constructed by select and expand query values. Also, you can return the non-generic response directly as an IActionResult, i.e. as an JsonResult that will convert to an application/json.

Caveats 😷

OData and enums

Filtering with OData against enums and their backing number values does not work at the moment, but filtering against a string value does: OData/WebApi#1186

// this filter will work, where ActiveState is an enum backed with numbers:
var qo = new ODataQuery(filter: "ActiveState eq 'Active'");

// this filter will NOT work, as 1 can not be converted to an enum, the converter is still not supported
var qo = new ODataQuery(filter: "ActiveState eq 1");

Future work planned 🚧

A list of possible items to be resolved in the future updates of this project.

  • update the used OData library to a stable release
  • update used libraries to newest and more stable versions
  • simplify how options are implemented, possibly change to IOption .NET Core suggested interface
  • invastigate if a GraphQL and Dynamic LINQ integration can be introduced as additional providers such as OData

Very useful links β›“ + HOW TO ❓

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment