Skip to content

Commit

Permalink
Merge pull request #246 from Avanade/feature/structV8
Browse files Browse the repository at this point in the history
feat(Liquid.Repository.Odata): Create Liquid.Repository.Odata cartridge
  • Loading branch information
lucianareginalino authored Jun 26, 2024
2 parents 85e830e + d7047a9 commit 3c57e3f
Show file tree
Hide file tree
Showing 18 changed files with 877 additions and 1 deletion.
26 changes: 26 additions & 0 deletions .github/workflows/liquid-ci-cd-repository-odata.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# CI & CD workflow
name: CI/CD - Liquid.Repository.OData Cartridge for Liquid Application Framework

on:
push:
branches: [ main, releases/v2.X.X, releases/v6.X.X ]
paths:
- 'src/Liquid.Repository.OData/**'

pull_request:
branches: [ main, releases/** ]
types: [opened, synchronize, reopened]
paths:
- 'src/Liquid.Repository.OData/**'

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

jobs:
call-reusable-build-workflow:
uses: Avanade/Liquid-Application-Framework/.github/workflows/base-liquid-ci-and-cd.yml@main
with:
component_name: Liquid.Repository.OData
secrets:
sonar_token: ${{ secrets.SONAR_TOKEN_ODATA }}
nuget_token: ${{ secrets.PUBLISH_TO_NUGET_ORG }}
15 changes: 14 additions & 1 deletion Liquid.Application.Framework.sln
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.Adapter.Dataverse.Te
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.Adapter.AzureStorage.Tests", "test\Liquid.Adapter.AzureStorage.Tests\Liquid.Adapter.AzureStorage.Tests.csproj", "{53341B04-6D30-4137-943B-20D8706351E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liquid.ChatCompletions.OpenAi", "src\Liquid.ChatCompletions.OpenAi\Liquid.ChatCompletions.OpenAi.csproj", "{CB199ED6-3D1D-4E12-A15F-597B6A0BA564}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.ChatCompletions.OpenAi", "src\Liquid.ChatCompletions.OpenAi\Liquid.ChatCompletions.OpenAi.csproj", "{CB199ED6-3D1D-4E12-A15F-597B6A0BA564}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Liquid.Repository.OData", "src\Liquid.Repository.OData\Liquid.Repository.OData.csproj", "{60AE2AF5-D84C-4A8B-AA36-FD58A6A423D7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liquid.Repository.OData.Tests", "test\Liquid.Repository.OData.Tests\Liquid.Repository.OData.Tests.csproj", "{70A43D24-4905-4A16-8CE2-165F73243B8D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -193,6 +197,14 @@ Global
{CB199ED6-3D1D-4E12-A15F-597B6A0BA564}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB199ED6-3D1D-4E12-A15F-597B6A0BA564}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB199ED6-3D1D-4E12-A15F-597B6A0BA564}.Release|Any CPU.Build.0 = Release|Any CPU
{60AE2AF5-D84C-4A8B-AA36-FD58A6A423D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60AE2AF5-D84C-4A8B-AA36-FD58A6A423D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60AE2AF5-D84C-4A8B-AA36-FD58A6A423D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60AE2AF5-D84C-4A8B-AA36-FD58A6A423D7}.Release|Any CPU.Build.0 = Release|Any CPU
{70A43D24-4905-4A16-8CE2-165F73243B8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{70A43D24-4905-4A16-8CE2-165F73243B8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70A43D24-4905-4A16-8CE2-165F73243B8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70A43D24-4905-4A16-8CE2-165F73243B8D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -212,6 +224,7 @@ Global
{DED98401-EAED-4BE0-84A9-494D77B0E70D} = {BDB77DF2-2D7D-4363-BB2B-D6A3A8A69C7B}
{5B0DC38B-5BC9-4DAC-8527-8D1C33E97247} = {BDB77DF2-2D7D-4363-BB2B-D6A3A8A69C7B}
{53341B04-6D30-4137-943B-20D8706351E8} = {BDB77DF2-2D7D-4363-BB2B-D6A3A8A69C7B}
{70A43D24-4905-4A16-8CE2-165F73243B8D} = {BDB77DF2-2D7D-4363-BB2B-D6A3A8A69C7B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1D003939-9797-4F37-B391-10047A780641}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Liquid.Core.Entities;
using Liquid.Core.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Liquid.Repository.OData.Extensions
{
/// <summary>
/// Extension methods for <see cref="ILiquidRepository{TEntity, TIdentifier}"/>.
/// </summary>
public static class ILiquidRepositoryExtension
{
/// <summary>
/// Set the token to perform operations.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TIdentifier"></typeparam>
/// <param name="repository"></param>
/// <param name="token"></param>
/// <returns></returns>
public static ILiquidRepository<TEntity, TIdentifier> SetODataAuthenticationHeader<TEntity, TIdentifier>(this ILiquidRepository<TEntity, TIdentifier> repository, string token) where TEntity : LiquidEntity<TIdentifier>, new()
{
var oDataRepository = repository as ODataRepository<TEntity, TIdentifier>;

oDataRepository.SetToken(token);

Check warning on line 28 in src/Liquid.Repository.OData/Extensions/ILiquidRepositoryExtension.cs

View workflow job for this annotation

GitHub Actions / call-reusable-build-workflow / build

Dereference of a possibly null reference.

return repository;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Liquid.Core.Entities;
using Liquid.Core.Extensions.DependencyInjection;
using Liquid.Core.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Liquid.Repository.OData.Extensions
{
/// <summary>
/// Extension methods for IServiceCollection.
/// </summary>
public static class IServiceCollectionExtensions
{
/// <summary>
/// Registers a <see cref="ODataRepository{TEntity, TIdentifier}"/> for the entity <typeparamref name="TEntity"/>,
/// and a <see cref="ODataClientFactory"/> if not previously registered.
/// </summary>
/// <typeparam name="TEntity">Type of entity that the repository should correspond to</typeparam>
/// <typeparam name="TIdentifier">Entity identifier type.</typeparam>
/// <param name="services">Extended ServiceCollection object.</param>
/// <param name="sectionName">Name of the configuration section where all entities have their repository settings configured.</param>
/// <param name="entityName">Name of the entity in the database that the repository should correspond to.</param>
public static IServiceCollection AddLiquidOdataRepository<TEntity, TIdentifier>(this IServiceCollection services, string sectionName, string entityName)
where TEntity : LiquidEntity<TIdentifier>, new()
{
services.TryAddSingleton<IODataClientFactory, ODataClientFactory>();

services.AddOptions<ODataOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection(sectionName).Bind(settings);
});

services.AddScoped<ILiquidRepository<TEntity, TIdentifier>>((provider) =>
{
return ActivatorUtilities.CreateInstance<ODataRepository<TEntity, TIdentifier>>(provider, entityName);
});

return services;
}
}
}
22 changes: 22 additions & 0 deletions src/Liquid.Repository.OData/IODataClientFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Simple.OData.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Liquid.Repository.OData
{
/// <summary>
/// Defines an object with the ability to create an OData client.
/// </summary>
public interface IODataClientFactory
{
/// <summary>
/// Create an OData client.
/// </summary>
/// <param name="entityName">The entity name.</param>
/// <param name="token">Authorization token.</param>
IODataClient CreateODataClientAsync(string entityName, string token);
}
}
39 changes: 39 additions & 0 deletions src/Liquid.Repository.OData/Liquid.Repository.OData.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>Liquid.Repository.OData</PackageId>
<Nullable>enable</Nullable>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Authors>Avanade Brazil</Authors>
<Company>Avanade Inc.</Company>
<Product>Liquid Application Framework</Product>
<Copyright>Avanade 2019</Copyright>
<PackageProjectUrl>https://github.com/Avanade/Liquid-Application-Framework</PackageProjectUrl>
<PackageIcon>logo.png</PackageIcon>
<Version>8.0.0-beta-01</Version>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<DebugType>Full</DebugType>
<Description>
Liquid Repository adapter for Odata apis.
This component is part of Liquid Application Framework.
</Description>
</PropertyGroup>

<ItemGroup>
<None Include="..\..\logo.png" Link="logo.png">
<PackagePath></PackagePath>
<Pack>True</Pack>
</None>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Liquid.Core" Version="8.0.0-alpha-05" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageReference Include="Simple.OData.Client" Version="6.0.1" />
</ItemGroup>

</Project>
65 changes: 65 additions & 0 deletions src/Liquid.Repository.OData/ODataClientFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Microsoft.Extensions.Options;
using Simple.OData.Client;

namespace Liquid.Repository.OData
{
///<inheritdoc/>
public class ODataClientFactory : IODataClientFactory
{
private readonly IOptions<ODataOptions> _options;

/// <summary>
/// Initialize a new instance of <see cref="ODataClientFactory"/>
/// </summary>
/// <param name="options"></param>
public ODataClientFactory(IOptions<ODataOptions> options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
}

///<inheritdoc/>
public IODataClient CreateODataClientAsync(string entityName, string token)
{
var settings = _options?.Value?.Settings.FirstOrDefault(x => x.EntityName == entityName);

Check warning on line 23 in src/Liquid.Repository.OData/ODataClientFactory.cs

View workflow job for this annotation

GitHub Actions / call-reusable-build-workflow / build

"Find" method should be used instead of the "FirstOrDefault" extension method. (https://rules.sonarsource.com/csharp/RSPEC-6602)

if (settings == null)
throw new ArgumentOutOfRangeException(nameof(entityName));

var client = new ODataClient(GetODataSettings(settings, token));

return client;
}

/// <summary>
///Initialize a new instance of <see cref="ODataClientSettings"/>
/// </summary>
/// <param name="settings">OData settings.</param>
/// <param name="token"> Authorization token.</param>
/// <returns></returns>
private static ODataClientSettings GetODataSettings(ODataSettings? settings, string token)
{
var odataSettings = new ODataClientSettings(new Uri(settings.BaseUrl));

Check warning on line 41 in src/Liquid.Repository.OData/ODataClientFactory.cs

View workflow job for this annotation

GitHub Actions / call-reusable-build-workflow / build

Dereference of a possibly null reference.

odataSettings.BeforeRequest = (message) =>
{
message.Headers.Add("Authorization", token);
};

if (!settings.ValidateCert)
{
var handler = new HttpClientHandler();

Check warning on line 50 in src/Liquid.Repository.OData/ODataClientFactory.cs

View workflow job for this annotation

GitHub Actions / call-reusable-build-workflow / build

Remove the unused local variable 'handler'. (https://rules.sonarsource.com/csharp/RSPEC-1481)

odataSettings.OnApplyClientHandler = (handler) =>
{
handler.ServerCertificateCustomValidationCallback +=

Check warning on line 54 in src/Liquid.Repository.OData/ODataClientFactory.cs

View workflow job for this annotation

GitHub Actions / call-reusable-build-workflow / build

Enable server certificate validation on this SSL/TLS connection (https://rules.sonarsource.com/csharp/RSPEC-4830)
(sender, certificate, chain, errors) =>
{
return true;
};
};
}

return odataSettings;
}
}
}
116 changes: 116 additions & 0 deletions src/Liquid.Repository.OData/ODataRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using Liquid.Core.Entities;
using Liquid.Core.Interfaces;
using Simple.OData.Client;
using System.Linq.Expressions;

namespace Liquid.Repository.OData
{
///<inheritdoc/>
public class ODataRepository<TEntity, TIdentifier> : ILiquidRepository<TEntity, TIdentifier> where TEntity : LiquidEntity<TIdentifier>, new()
{
///<inheritdoc/>
public ILiquidDataContext DataContext => throw new NotImplementedException();

private readonly IODataClientFactory _clientFactory;
private readonly string _entityName;
private string _token;

/// <summary>
/// Initialize a new instance of <see cref="ODataRepository{TEntity, TIdentifier}"/>
/// </summary>
/// <param name="clientFactory"> Factory to create OData client.</param>
/// <param name="entityName"> Name of the entity to be used in the repository.</param>
/// <exception cref="ArgumentNullException"></exception>
public ODataRepository(IODataClientFactory clientFactory, string entityName)
{
_clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));
_entityName = entityName ?? throw new ArgumentNullException(nameof(entityName));
}

/// <summary>
/// Set the token to perform operations.
/// </summary>
/// <param name="token">Token to be set.</param>
public void SetToken(string token)
{
_token = token;
}

///<inheritdoc/>
public async Task AddAsync(TEntity entity)
{
if (string.IsNullOrEmpty(_token))
{
throw new InvalidOperationException("Token is required to perform this operation.");

Check warning on line 44 in src/Liquid.Repository.OData/ODataRepository.cs

View workflow job for this annotation

GitHub Actions / call-reusable-build-workflow / build

Define a constant instead of using this literal 'Token is required to perform this operation.' 6 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
}

var client = _clientFactory.CreateODataClientAsync(_entityName, _token);

await client.For<TEntity>().Set(entity).InsertEntryAsync();
}

///<inheritdoc/>
public async Task<IEnumerable<TEntity>> FindAllAsync()
{
if (string.IsNullOrEmpty(_token))
{
throw new InvalidOperationException("Token is required to perform this operation.");
}

var client = _clientFactory.CreateODataClientAsync(_entityName, _token);

return await client.For<TEntity>().FindEntriesAsync();
}

///<inheritdoc/>
public async Task<TEntity> FindByIdAsync(TIdentifier id)
{
if (string.IsNullOrEmpty(_token))
{
throw new InvalidOperationException("Token is required to perform this operation.");
}
var client = _clientFactory.CreateODataClientAsync(_entityName, _token);

return await client.For<TEntity>().Key(id).FindEntryAsync();

Check warning on line 74 in src/Liquid.Repository.OData/ODataRepository.cs

View workflow job for this annotation

GitHub Actions / call-reusable-build-workflow / build

Possible null reference argument for parameter 'entryKey' in 'IBoundClient<TEntity> IFluentClient<TEntity, IBoundClient<TEntity>>.Key(params object[] entryKey)'.

Check warning on line 74 in src/Liquid.Repository.OData/ODataRepository.cs

View workflow job for this annotation

GitHub Actions / call-reusable-build-workflow / build

Review this call, which partially matches an overload without 'params'. The partial match is 'IBoundClient<TEntity> IFluentClient<TEntity, IBoundClient<TEntity>>.Key(IEnumerable<object> entryKey)'. (https://rules.sonarsource.com/csharp/RSPEC-3220)
}

///<inheritdoc/>
public async Task RemoveByIdAsync(TIdentifier id)
{
if (string.IsNullOrEmpty(_token))
{
throw new InvalidOperationException("Token is required to perform this operation.");
}

var client = _clientFactory.CreateODataClientAsync(_entityName, _token);

await client.For<TEntity>().Key(id).DeleteEntryAsync();

Check warning on line 87 in src/Liquid.Repository.OData/ODataRepository.cs

View workflow job for this annotation

GitHub Actions / call-reusable-build-workflow / build

Possible null reference argument for parameter 'entryKey' in 'IBoundClient<TEntity> IFluentClient<TEntity, IBoundClient<TEntity>>.Key(params object[] entryKey)'.

Check warning on line 87 in src/Liquid.Repository.OData/ODataRepository.cs

View workflow job for this annotation

GitHub Actions / call-reusable-build-workflow / build

Review this call, which partially matches an overload without 'params'. The partial match is 'IBoundClient<TEntity> IFluentClient<TEntity, IBoundClient<TEntity>>.Key(IEnumerable<object> entryKey)'. (https://rules.sonarsource.com/csharp/RSPEC-3220)
}

///<inheritdoc/>
public async Task UpdateAsync(TEntity entity)
{
if (string.IsNullOrEmpty(_token))
{
throw new InvalidOperationException("Token is required to perform this operation.");
}

var client = _clientFactory.CreateODataClientAsync(_entityName, _token);

await client.For<TEntity>().Set(entity).UpdateEntryAsync();
}

///<inheritdoc/>
public async Task<IEnumerable<TEntity>> WhereAsync(Expression<Func<TEntity, bool>> whereClause)
{
if (string.IsNullOrEmpty(_token))
{
throw new InvalidOperationException("Token is required to perform this operation.");
}

var client = _clientFactory.CreateODataClientAsync(_entityName, _token);

return await client.For<TEntity>().Filter(whereClause).FindEntriesAsync();
}
}
}
Loading

0 comments on commit 3c57e3f

Please sign in to comment.