Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Code #1

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
beta_Version: ${{ steps.gitversion.outputs.nuGetVersion }}
branchName: ${{ steps.gitversion.outputs.branchName }}
env:
working-directory: /home/runner/work/Schemio.Data/Schemio.Data
working-directory: /home/runner/work/Schemio.Object/Schemio.Object

steps:
- name: Step-01 Install GitVersion
Expand Down Expand Up @@ -87,7 +87,7 @@ jobs:
env:
github-token: '${{ secrets.GITHUB_TOKEN }}'
nuget-token: '${{ secrets.NUGET_API_KEY }}'
working-directory: /home/runner/work/Schemio.Data/Schemio.Data
working-directory: /home/runner/work/Schemio.Object/Schemio.Object
steps:
- name: Step-01 Retrieve Build Artifacts
uses: actions/download-artifact@v3
Expand All @@ -103,7 +103,7 @@ jobs:

- name: Step-03 Release to Nuget Org
if: ${{ startsWith(github.head_ref, 'release/')}}
run: dotnet nuget push ${{env.working-directory}}/src/Schemio.Data/bin/Release/*.nupkg --skip-duplicate --api-key ${{ env.nuget-token }} --source https://api.nuget.org/v3/index.json
run: dotnet nuget push ${{env.working-directory}}/src/Schemio.Object/bin/Release/*.nupkg --skip-duplicate --api-key ${{ env.nuget-token }} --source https://api.nuget.org/v3/index.json

Release:
name: Release to Nuget
Expand All @@ -119,15 +119,15 @@ jobs:
# dotnet-version: '6.0.x'

# Publish
- name: publish Schemio.Data package
- name: publish Schemio.Object package
id: publish_nuget
uses: Rebel028/[email protected]
with:
# Filepath of the project to be packaged, relative to root of repository
PROJECT_FILE_PATH: Schemio.Data/Schemio.Data.csproj
PROJECT_FILE_PATH: Schemio.Object/Schemio.Object.csproj

# NuGet package id, used for version detection & defaults to project name
PACKAGE_NAME: Schemio.Data
PACKAGE_NAME: Schemio.Object

# Filepath with version info, relative to root of repository & defaults to PROJECT_FILE_PATH
# VERSION_FILE_PATH: Directory.Build.props
Expand Down
15 changes: 15 additions & 0 deletions GitVersion.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
next-version: 1.0.1
tag-prefix: '[vV]'
mode: ContinuousDeployment
branches:
master:
regex: ^master$
release:
regex: ^release$
develop:
regex: ^develop$|^dev$
tag: beta
pull-request:
tag: beta
ignore:
sha: []
188 changes: 184 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,185 @@
# Schemio.Data
[![NuGet version](https://badge.fury.io/nu/Schemio.Data.svg)](https://badge.fury.io/nu/Schemio.Data) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/NinjaRocks/Schemio.Data/blob/master/License.md) [![CI](https://github.com/NinjaRocks/Data2Xml/actions/workflows/CI.yml/badge.svg)](https://github.com/NinjaRocks/Data2Xml/actions/workflows/CI.yml) [![GitHub Release](https://img.shields.io/github/v/release/ninjarocks/Data2Xml?logo=github&sort=semver)](https://github.com/ninjarocks/Data2Xml/releases/latest)
[![CodeQL](https://github.com/NinjaRocks/Schemio.Data/actions/workflows/CodeQL.yml/badge.svg)](https://github.com/NinjaRocks/Schemio.Data/actions/workflows/CodeQL.yml) [![.Net Stardard](https://img.shields.io/badge/.Net%20Standard-2.1-blue)](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
# Schemio.Object
[![NuGet version](https://badge.fury.io/nu/Schemio.Object.svg)](https://badge.fury.io/nu/Schemio.Object) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/NinjaRocks/Schemio.Object/blob/master/License.md) [![CI](https://github.com/NinjaRocks/Data2Xml/actions/workflows/CI.yml/badge.svg)](https://github.com/NinjaRocks/Data2Xml/actions/workflows/CI.yml) [![GitHub Release](https://img.shields.io/github/v/release/ninjarocks/Data2Xml?logo=github&sort=semver)](https://github.com/ninjarocks/Data2Xml/releases/latest)
[![CodeQL](https://github.com/NinjaRocks/Schemio.Object/actions/workflows/CodeQL.yml/badge.svg)](https://github.com/NinjaRocks/Schemio.Object/actions/workflows/CodeQL.yml) [![.Net Stardard](https://img.shields.io/badge/.Net%20Standard-2.1-blue)](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
--
### .Net Standard 2.1 Library to map data to Entity using Schema Paths (Like XPath or JsonPath).
## What is Schemio?
`Schemio` is a .Net utility that can be used to fetch entities by specifying sections of object graph for hydrating data.
> Supports XPath & JsonPath for object schema paths.

## When to use Schemio?
Schemio is a perfect utility when you need to fetch a large entity from data source. Ideally, you may not require all of the entity data but only sections of the object graph for different fetches.
> Example use case is document generation which may require only templated sections of client data to be fetched for different document templates in context.

## How to use Schemio?
You could use Schemio out of the box or extend the utility in order to suit your custom needs.
> To use schemio you need to
> - Setup the entity to be fetched using DataProvider.
> - Construct the DataProvider with required dependencies.

### Entity Setup
* Define the `entity` to be fetched using `DataProvider` - which is basically a class with nested typed properties.
* Define the `entity schema` with `query` and `transformer` pairs mappings to entity's object graph. The relevant query and transformer pairs will execute in the order of their nesting when their mapped `schema paths` are included in the `request` parameter of the DataProvider.
* `Query` is an implementation to fetch `data` for the entity object graph from the underlying data storage supported by the chosen `QueryEngine`. QueryEngine is an implementation of `IQueryEngine` to execute queries against supported data source.
* `Transformer` is an implementation to transform the data fetched by the associated query to mapped section of the object graph.
#### 1. Entity
> Step 1 - To mark the class as Entity using schemio, implement the class from `IEntity` interface. Bear in mind this is the root entity to be fetched.

Below is an example `Customer` entity we want to fetch using schemio.

```
public class Customer : IEntity
{
public int CustomerId { get; set; }
public string CustomerCode { get; set; }
public string CustomerName { get; set; }
public Communication Communication { get; set; }
public Order[] Orders { get; set; }
}
```
Example Customer class with XSD Schema
```
To Do
```
There are three levels of nesting in the object graph for customer class above.
- Level 1 with paths: `Customer/CustomerId`, `Customer/CustomerCode`, `Customer/CustomerName`
- Level 2 with paths: `Customer/Communication` and `Customer/Orders`
- Level 3 with paths: `Customer/Orders/Order/Items`

#### 2. Entity Schema Definition
> Step 2 - Define entity schema configuration which is basically a hierarchy of query/transformer pairs mapping to the schema paths pointing to the object graph of the entity in context.
In query/transformer setup, the output of the query serves as the input to the transformer to map data to linked object graph of the entity in context.
You could nest query/transformer pairs in a parent/child setup. In which case the output of the parent query would become the input to the child query to resolve query paramter.

To define Entity schema, implement `IEntitySchema<T>` interface where T is entity in context. The `query/transformer` mappings can be `nested` to `5` levels down.

You could map multiple schema paths to a given query/transformer pair. Currently, XPath and JSONPath schema paths are supported.

If you need to support custom schema language for mapping to object graph, then use the custom paths in entity schema definition however you may need to provide custom implementation of `ISchemaPathMatcher` interface.
```
public interface ISchemaPathMatcher
{
bool IsMatch(string inputPath, ISchemaPaths configuredPaths);
}
```

Example Entity Schema Definition
> The `Customer` entity with `three` levels of `nesting` is configured below in `CustomerSchema` definition to show `query/transformer` pairs nested accordingly mapping to object graph using the XPath definitions.

```
internal class CustomerSchema : IEntitySchema<Customer>
{
private IEnumerable<Mapping<Customer, IQueryResult>> mappings;

public IEnumerable<Mapping<Customer, IQueryResult>> Mappings => mappings;

public CustomerSchema()
{
// Create an object mapping graph of query and transformer pairs using xpaths.

mappings = CreateSchema.For<Customer>()
.Map<CustomerQuery, CustomerTransform>(For.Paths("customer/id", "customer/customercode", "customer/customername"),
customer => customer.Dependents
.Map<CustomerCommunicationQuery, CustomerCommunicationTransform>(For.Paths("customer/communication"))
.Map<CustomerOrdersQuery, CustomerOrdersTransform>(For.Paths("customer/orders"),
customerOrders => customerOrders.Dependents
.Map<CustomerOrderItemsQuery, CustomerOrderItemsTransform>(For.Paths("customer/orders/order/items")))
).Complete();
}
}
```
<img width="1202" alt="image" src="https://github.com/TechNinjaLabs/Schemio.Object/assets/6259981/44af21e3-60d9-4452-b44b-a6aaa1a10d4f">


#### 2.1 Query Class
The purpose of a query class is to execute to fetch data from data source when mapped schema path(s) are included in the request parameter of data provider.
- To define a query you need to implement from `BaseQuery<TQueryParameter, TQueryResult>` where `TQueryParameter` is the query parameter and `TQueryResult` is the query result.
- `TQueryParameter` is basically the class that holds the `inputs` required by the query for execution.
- `TQueryResult` is the result that will be obtained from executing the query.
- You can run the query in `Parent` or `Child` (nested) mode. The parent/child relationship is achieved by configuring the query accordingly in entity schema definition. See `CustomerSchema` above.
- The query parameter needs to be resolved before executing the query with QueryEngine.
- In `parent` mode, the query parameter is resolved using the `request/context` parameter passed to data provider class.
- In `child` mode, the query parameter is resolved using the `query result` of the `parent query` as stiched in the entity schema configuration. You could have a maximum of `5` levels of children query nestings.

See example `CustomerQuery` implemented to run in parent mode below.
```
public class CustomerQuery : BaseQuery<CustomerParameter, CustomerResult>
{
public override void ResolveParameterInParentMode(IDataContext context)
{
// Executes as Parent or Level 1 query.
// The query parameter is resolved using context parameter of data provider class.

var customer = (CustomerContext)context;
QueryParameter = new CustomerParameter
{
CustomerId = customer.CustomerId
};
}

public override void ResolveParameterInChildMode(IDataContext context, IQueryResult parentQueryResult)
{
// Does not execute as child to any query. Hence has no implementation provided.
}
}
```
See example `CustomerCommunicationQuery` implemented to run as child or nested query to customer query below. Please see `CustomerSchema` definition above for parent/child configuration setup.
```
internal class CustomerCommunicationQuery : BaseQuery<CustomerParameter, CommunicationResult>
{
public override void ResolveParameterInParentMode(IDataContext context)
{
// Does not execute as Parent or Level 1 query. Hence has no implementation provided.
}

public override void ResolveParameterInChildMode(IDataContext context, IQueryResult parentQueryResult)
{
// Execute as child to customer query.
// The result from parent customer query is used to resolve the query parameter of the nested communication query.

var customer = (CustomerResult)parentQueryResult;
QueryParameter = new CustomerParameter
{
CustomerId = customer.Id
};
}
}
```
Please Note: The above query implementation is basic and could vary with different implementations of the QueryEngine.
> Please see Query engine provider specific implementation of queries below.


#### 2.2 Tranformer Class
The purpose of the transformer class is to transform the data fetched by the linked query class to mapped object graph of the entity.

To define a transformer class, you need to implement `BaseTransformer<TD, T>`
- where T is Entity implementing `IEntity`. eg. Customer.
- where TD is Query Result from associated Query implementing `IQueryResult` in EntitySchema definition. This is the query result obtained from the query, the transformer will consume to map to the relevant object graph of the Entity.

> Please Note: Every `Query` type in the `EntitySchema` definition should have a complementing `Transformer` type.

Below is the snippet from `CustomerSchema` definition.
> .Map<CustomerQuery, CustomerTransform>(For.Paths("customer/id", "customer/customercode", "customer/customername"))

The transformer should `map` `data` to only the `schema` mapped `sections` of the `object graph`.


In below example, `CustomerTransformer` (transformer) is implemented to transform `Customer` (entity) with `CustomerResult` (query result) obtained from `CustomerQuery` (query) execution.

The transformer maps data to only `XPath` mapped sections of Customer object grapgh - `customer/id`, `customer/customercode`, `customer/customername`

```
public class CustomerTransform : BaseTransformer<CustomerResult, Customer>
{
public override Customer Transform(CustomerResult queryResult, Customer entity)
{
var customer = entity ?? new Customer();
customer.CustomerId = queryResult.Id;
customer.CustomerName = queryResult.CustomerName;
customer.CustomerCode = queryResult.CustomerCode;
return customer;
}
}
```

### DataProvider Setup
> coming soon
24 changes: 23 additions & 1 deletion Schemio.Object.sln
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{AF
.gitignore = .gitignore
.github\workflows\CI.yml = .github\workflows\CI.yml
.github\workflows\CodeQL.yml = .github\workflows\CodeQL.yml
README.md = README.md
GitVersion.yml = GitVersion.yml
LICENSE.md = LICENSE.md
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schemio.Object.SQL", "src\Schemio.Object.SQL\Schemio.Object.SQL.csproj", "{1A0CB973-23C9-4A17-905E-59510CD18932}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Schemio.Object.SQL.Tests", "tests\Schemio.Object.SQL.Tests\Schemio.Object.SQL.Tests.csproj", "{CBFE3D90-7FB2-4CB5-8B74-C8664F0173CA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Schemio.Object.EF", "src\Schemio.Object.EF\Schemio.Object.EF.csproj", "{6B92CC17-B7DB-446F-BF2F-A93696D48B5D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -34,13 +41,28 @@ Global
{FDB00281-8B65-4A17-9F1F-B97865544BEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FDB00281-8B65-4A17-9F1F-B97865544BEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FDB00281-8B65-4A17-9F1F-B97865544BEF}.Release|Any CPU.Build.0 = Release|Any CPU
{1A0CB973-23C9-4A17-905E-59510CD18932}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A0CB973-23C9-4A17-905E-59510CD18932}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A0CB973-23C9-4A17-905E-59510CD18932}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A0CB973-23C9-4A17-905E-59510CD18932}.Release|Any CPU.Build.0 = Release|Any CPU
{CBFE3D90-7FB2-4CB5-8B74-C8664F0173CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CBFE3D90-7FB2-4CB5-8B74-C8664F0173CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CBFE3D90-7FB2-4CB5-8B74-C8664F0173CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CBFE3D90-7FB2-4CB5-8B74-C8664F0173CA}.Release|Any CPU.Build.0 = Release|Any CPU
{6B92CC17-B7DB-446F-BF2F-A93696D48B5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B92CC17-B7DB-446F-BF2F-A93696D48B5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B92CC17-B7DB-446F-BF2F-A93696D48B5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B92CC17-B7DB-446F-BF2F-A93696D48B5D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{6F017146-B95A-4081-9CC0-B0245F78D72B} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804}
{FDB00281-8B65-4A17-9F1F-B97865544BEF} = {07BAE427-96CF-4F9B-80A9-48CFB0A89CF3}
{1A0CB973-23C9-4A17-905E-59510CD18932} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804}
{CBFE3D90-7FB2-4CB5-8B74-C8664F0173CA} = {07BAE427-96CF-4F9B-80A9-48CFB0A89CF3}
{6B92CC17-B7DB-446F-BF2F-A93696D48B5D} = {F41DA3D8-A0E9-4A05-8A35-78313C0F5804}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C0FF62D6-1374-4939-A546-432862338528}
Expand Down
17 changes: 17 additions & 0 deletions src/Schemio.Object.EF/BaseSQLQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;

namespace Schemio.Object.EF
{
public abstract class BaseSQLQuery<TQueryParameter, TQueryResult>
: BaseQuery<TQueryParameter, TQueryResult>, ISQLQuery
where TQueryParameter : IQueryParameter
where TQueryResult : IQueryResult
{
/// <summary>
/// Get query delegate with implementation to return query result.
/// Delegate returns a collection from db.
/// </summary>
/// <returns>Func<DbContext, IEnumerable<IQueryResult>></returns>
public abstract Func<DbContext, IEnumerable<IQueryResult>> GetQuery();
}
}
42 changes: 42 additions & 0 deletions src/Schemio.Object.EF/EFQueryEngine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore;

namespace Schemio.Object.EF
{
public class EFQueryEngine<T> : IQueryEngine where T : DbContext
{
private readonly IDbContextFactory<T> _dbContextFactory;

public EFQueryEngine(IDbContextFactory<T> _dbContextFactory)
=> this._dbContextFactory = _dbContextFactory;

public IQueryResult[] Run(IQueryList queryList, IDataContext context)
{
if (queryList?.Queries == null)
return Array.Empty<IQueryResult>();

var queries = queryList.Queries.Cast<ISQLQuery>();

if (!queries.Any())
return Array.Empty<IQueryResult>();

var output = new List<IQueryResult>();

using (var dbcontext = _dbContextFactory.CreateDbContext())
{
foreach (var query in queries)
{
var queryDelegate = query.GetQuery();
if (queryDelegate == null)
continue;

var results = queryDelegate(dbcontext);
if (results == null)
continue;

output.AddRange(results);
}
return output.ToArray();
}
}
}
}
20 changes: 20 additions & 0 deletions src/Schemio.Object.EF/IDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Schemio.Object.EF
{
public interface IDbContext
{
//DbSet<TEntity> Set<[DynamicallyAccessedMembers(IEntityType.DynamicallyAccessedMemberTypes)] TEntity>()
//where TEntity : class;
DbSet<TEntity> Set<TEntity>() where TEntity : class;
}

internal class SchemioContext : DbContext, IDataContext
{
public string[] Paths { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

public decimal CurrentVersion => throw new NotImplementedException();
}
}
Loading
Loading