Health Checks UI display results in categories - asp.net-core

I have implemented Xabaril's Healthchecks in a simple .NET Core 3.0 API project. I am checking several URLs and SQL servers as a test and I successfully display the results in the HealthCheckUI. They all appear under the one 'category' that I defined in appSettings. Now, in the documentation you can see that you could have multiple of these categories, but it seems that this is meant to be used only when you display results from different sources.
What I want to do is have my API project check say 3 URIs and display them under a "Web" category, and then check 3 SQL servers and display them under a "SQL" category.
From this example here it seems that we can achieve the 2 different categories with this piece of code:
app
.UseHealthChecks("/health", new HealthCheckOptions
{
Predicate = _ => true
})
.UseHealthChecks("/healthz", new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
})
However in the Startup class, when we add checks we do not specify to which API endpoint they should be reported to so that we can categorize them:
services.AddHealthChecks().AddCheck...
Am I missing something or this UI client is not meant to be used like this?

I got this working in a Net Core 3.1 project using tags.
For example if you wanted a Database and API section in the Spa UI:
In ConfigureServices method of Startup.cs
services.AddHealthChecks()
.AddSqlServer("ConnectionString", name: "Application Db", tags: new[] { "database" })
.AddHangfire(options => { options.MinimumAvailableServers = 1; }, "Hangfire Db", tags: new[] { "database" })
.AddUrlGroup(new Uri("https://myapi.com/api/person"), HttpMethod.Get, "Person Endpoint", tags: new[] { "api" })
.AddUrlGroup(new Uri("https://myapi.com/api/organisation"), HttpMethod.Get, "Org Endpoint", tags: new[] { "api" })
.AddUrlGroup(new Uri("https://myapi.com/api/address"), HttpMethod.Get, "Address Endpoint", tags: new[] { "api" });
In Configure method of Startup.cs
config.MapHealthChecks("/health-check-database", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("database"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
config.MapHealthChecks("/health-check-api", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("api"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
Then appsettings.json something like:
"HealthChecksUI": {
"HealthChecks": [
{
"Name": "Database Health Checks",
"Uri": "https://myapi.com/api/health-check-database"
},
{
"Name": "API Health Checks",
"Uri": "https://myapi.com/api/health-check-api"
}
],
"Webhooks": [],
"EvaluationTimeinSeconds": 10,
"MinimumSecondsBetweenFailureNotifications": 60
}

Related

Filter products by category in a Shopware 6 Import/Export Profile

I created a Profile in shopware 6 with the api.
The body of the request is:
{
"id": "my99id2cb2784069938089a8a77ctest",
"label": "Label test",
"name": "name test",
"sourceEntity": "product",
"fileType": "text/csv",
"delimiter": ";",
"enclosure": "\"",
"createdAt": "2022-11-10T10:15:22Z",
"mapping": [{"key":"id","mappedKey":"id","position":0,"id":"someid4f7c6c478b8041cfd5fe0ae0c5"},{"key":"cover.media.url","mappedKey":"cover","position":1,"id":"someid0ffa7f48e6b135f6e754d6b93c"}],
"config": {"createEntities": false, "updateEntities": true}
}
Is there a way to specify that i want only product from certain categories? Not all of them
No, this currently isn't possible with just the profile and existing endpoints. You'll have to implement a custom api endpoint.
If you are able to do that follow the steps from this answer until you first retrieve a logId. Then instead of starting the process using the existing endpoint, request your new custom endpoint. The implementation could look similar to this:
/**
* #Route(defaults={"_routeScope"={"api"}})
*/
class CustomExportApiController extends AbstractController
{
private ImportExportFactory $importExportFactory;
public function __construct(ImportExportFactory $importExportFactory)
{
$this->importExportFactory = $importExportFactory;
}
/**
* #Route("/api/_action/custom/export/{logId}/{categoryId}", name="api.action.custom.export", methods={"POST"})
*/
public function customProductExport(string $logId, string $categoryId, Context $context): JsonResponse
{
$importExport = $this->importExportFactory->create($logId, 50, 50);
$logEntity = $importExport->getLogEntity();
if ($logEntity->getState() === Progress::STATE_ABORTED) {
return new JsonResponse(['success' => false]);
}
$criteria = new Criteria();
$criteria->addFilter(new EqualsAnyFilter('categoryTree', [$categoryId]));
$offset = 0;
do {
$progress = $importExport->export($context, $criteria, $offset);
$offset = $progress->getOffset();
} while (!$progress->isFinished());
return new JsonResponse(['success' => true]);
}
}
After calling your custom endpoint proceed with the steps as described in the answer linked above.

How to use a part of intercepted endpoint as a variable in my stub with Cypress

I am testing a frontend and I want to make my test more efficient
I have the following custom command:
cy.intercept('**/api/classification/dd86ac0a-ca23-413b-986c-535b6aad659c/items/**',
{ fixture: 'ItemsInEditor.json' }).as('ItemsInEditorStub')
This works correctly and is intercepts 25 times :). But the Id in the stub file has to be the same as in the requested Endpoint. Otherwise the frontEnd wilt not process it.
At this point I do not want to make 25 stubfiles in the fixture map.
In the printscreen you can see the different calls I need to intercept. The last ID I would like to save as variable and use it in the stub file
The Stub is like this:
{
"item": {
"version": 3,
"title": "Cars",
"rows": [],
"id": "dynamicIdBasedOnEndPoint" <- *Can we make it dynamic based on the ID in the endpoint*
},
"itemState": "Submitted"
}
UPDATE:
What I have for now is just the basic I guess:
cy.intercept('**/api/classification/*/items/**', {
body:
{
item: {
version: 3,
title: 'Cars',
rows: [],
id: '55eb5a28-24d8-4705-b465-8e1454f73ac8' //Still need this value to be dynamic and always the same as the intercepted '**'(wildcard)
},
itemState: "Submitted"
}
})
.as('ItemsInEditorStub')
cy.fixture('ItemsInEditor.json').then(ModFixture => {
cy.intercept('GET', '**/api/classification/**/items/id/**', (req) => {
const id = req.url.split('/').pop(); // last part of url path
ModFixture.item.id = id; // add the id dynamically
req.reply(ModFixture); // send altered fixture
})
}).as('ItemsInEditorStub')
Thanks to #Fody
You can make a dynamic fixture using javascript.
Ref Providing a stub response with req.reply()
cy.fixture('ItemsInEditor.json').then(fixture => {
cy.intercept('**/api/classification/dd86ac0a-ca23-413b-986c-535b6aad659c/items/**',
(req) => {
const id = req.url.split('/').pop(); // last part of url path
fixture.item.id = id; // add the id dynamically
req.reply(fixture); // send altered fixture
}
).as('ItemsInEditorStub')
})

GraphQL stitch and union

I have a need to 'aggregate' multiple graphQl services (with same schema) into single read-only (query only) service exposing data from all services. For example:
---- domain 1 ----
"posts": [
{
"title": "Domain 1 - First post",
"description": "Content of the first post"
},
{
"title": "Domain 1 - Second post",
"description": "Content of the second post"
}
]
---- domain 2 ----
"posts": [
{
"title": "Domain 2 - First post",
"description": "Content of the first post"
},
{
"title": "Domain 2 - Second post",
"description": "Content of the second post"
}
]
I understand that 'stitching' is not meant for UC's like this but more to combine different micro-services into same API. In order to have same types (names) into single API, I implemented 'poor man namespaces' by on-the-fly' appending domain name to all data types. However, I'm able only to make a query with two different types like this:
query {
domain_1_posts {
title
description
}
domain_2_posts {
title
description
}
}
but, it results with data set consist out of two arrays:
{
"data": {
"domain_1_posts": [
{ ...},
],
"domain_2_posts": [
{ ...},
]
}
}
I would like to hear your ideas what I can do to combine it into single dataset containing only posts?
One idea is to add own resolver that can call actual resolvers and combine results into single array (if that is supported at all).
Also, as a plan B, I could live with sending 'domain' param to query and then construct query toward first or second domain (but, to keep initial query 'domain-agnostic', e.g. without using domain namses in query itself?
Thanks in advance for all suggestions...
I manage to find solution for my use-case so, I'll leave it here in case that anyone bump into this thread...
As already mentioned, stitching should be used to compose single endpoint from multiple API segments (microservices). In case that you try to stitch schemas containing same types or queries, your request will be 'routed' to pre-selected instance (so, only one).
As #xadm suggested, key for 'merging' data from multiple schemas into singe data set is in using custom fetch logic for Link used for remote schema, as explained:
1) Define custom fetch function matching your business needs (simplified example):
const customFetch = async (uri, options) => {
// do not merge introspection query results!!!
// for introspection query always use predefined (first?) instance
if( operationType === 'IntrospectionQuery'){
return fetch(services[0].uri, options);
}
// array fecth calls to different endpoints
const calls = [
fetch(services[0].uri, options),
fetch(services[1].uri, options),
fetch(services[2].uri, options),
...
];
// execute calls in parallel
const data = await Promise.all(fetchCalls);
// do whatever you need to merge data according to your needs
const retData = customBusinessLogic();
// return new response containing merged data
return new fetch.Response(JSON.stringify(retData),{ "status" : 200 });
}
2) Define link using custom fetch function. If you are using identical schemas you don't need to create links to each instance, just one should be enough.
const httpLink = new HttpLink(services[0].uri, fetch: customFetch });
3) Use Link to create remote executable schema:
const schema = await introspectSchema(httpLink );
return makeRemoteExecutableSchema({
schema,
link: httpLink,
context: ({ req }) => {
// inject http request headers into context if you need them
return {
headers: {
...req.headers,
}
}
},
})
4) If you want to forward http headers all the way to the fetch function, use apollo ContextLink:
// link for forwarding headers through context
const contextLink = setContext( (request, previousContext) => {
if( previousContext.graphqlContext ){
return {
headers: {
...previousContext.graphqlContext.headers
}
}
}
}).concat(http);
Just to mention, dependencies used for this one:
const { introspectSchema, makeRemoteExecutableSchema, ApolloServer } = require('apollo-server');
const fetch = require('node-fetch');
const { setContext } = require('apollo-link-context');
const { HttpLink } = require('apollo-link-http');
I hope that it will be helfull to someone...

Load dynamic SAML schemes for IdentityServer4 using ComponentSpace

We have centralized a IdentityServer4 that will act as service provider and there are multiple identity providers like Active Directory, Google, Facebook and also other SAML providers based on each tenant. i.e., one service provider and multiple identity providers.
To load the openId configs from database I am exactly following the https://stackoverflow.com/a/56941908/2922388 and it is working as expected for openid and now I need to integrate SAML providers in the same way.
I went through the "SAMLv20.Core-evaluation" from componentspace and was able to do the integration using appsettings.json successfully.
But I am not sure how to integrate it programatically as given in https://stackoverflow.com/a/56941908/2922388.
Here is what I have done so far
public class AccountController : ControllerBase
{
private readonly IOptionsMonitorCache<OpenIdConnectOptions> _openIdOptionsCache;
private readonly IOptionsMonitorCache<SamlAuthenticationOptions> _samlOptionsCache;
private readonly OpenIdConnectPostConfigureOptions _postConfigureOptions;
private readonly SamlPostConfigureAuthenticationOptions _samlPostConfigureOptions;
public AccountController(
IOptionsMonitorCache<OpenIdConnectOptions> openidOptionsCache,
IOptionsMonitorCache<SamlAuthenticationOptions> samlOptionsCache,
OpenIdConnectPostConfigureOptions postConfigureOptions,
SamlPostConfigureAuthenticationOptions samlPostConfigureOptions
)
{
_openIdOptionsCache = openidOptionsCache;
_samlOptionsCache = samlOptionsCache;
_postConfigureOptions = postConfigureOptions;
_samlPostConfigureOptions = samlPostConfigureOptions;
}
private async Task<IEnumerable<AuthenticationScheme>> LoadAuthenticationSchemesByTenant(IEnumerable<AuthenticationScheme> schemes, AuthProviderSetting tenantAuthProviderSetting)
{
dynamic configJson = JsonConvert.DeserializeObject(tenantAuthProviderSetting.tenantConfigJson);
switch (tenantAuthProviderSetting.AuthenticationType)
{
case AuthenticationTypes.OpenID:
var oidcOptions = new OpenIdConnectOptions
{
SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
SignOutScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
SaveTokens = true,
Authority = configJson.Authority,
ClientId = configJson.ClientId,
ClientSecret = configJson.ClientSecret,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role",
ValidateIssuer = false
}
};
_schemeProvider.AddScheme(new AuthenticationScheme(tenantAuthProviderSetting.AuthenticationScheme, tenantAuthProviderSetting.DisplayName, typeof(OpenIdConnectHandler)));
_postConfigureOptions.PostConfigure(tenantAuthProviderSetting.AuthenticationScheme, oidcOptions);
_openIdOptionsCache.TryAdd(tenantAuthProviderSetting.AuthenticationScheme, oidcOptions);
schemes = await _schemeProvider.GetAllSchemesAsync();
break;
case AuthenticationTypes.SAML:
var samlOptions = new SamlAuthenticationOptions
{
PartnerName = delegate () { return "https://ExampleIdentityProvider"; },
SingleLogoutServicePath = "https://localhost:44313/SAML/SingleLogoutService",
// Not sure how to set other parameters here
};
_schemeProvider.AddScheme(new AuthenticationScheme(tenantAuthProviderSetting.AuthenticationScheme, tenantAuthProviderSetting.DisplayName, typeof(SamlAuthenticationHandler)));
_samlPostConfigureOptions.PostConfigure(tenantAuthProviderSetting.AuthenticationScheme, samlOptions);
_samlOptionsCache.TryAdd(tenantAuthProviderSetting.AuthenticationScheme, samlOptions);
schemes = await _schemeProvider.GetAllSchemesAsync();
break;
default:
schemes = await _schemeProvider.GetAllSchemesAsync();
break;
}
return schemes;
}
}
Here is the config through which I used to statically integrate with IdentityServer
"SAML": {
"$schema": "https://www.componentspace.com/schemas/saml-config-schema-v1.0.json",
"Configurations": [
{
"LocalServiceProviderConfiguration": {
"Name": "https://IdentityServer4",
"Description": "IdentityServer4",
"AssertionConsumerServiceUrl": "http://localhost:44380/SAML/AssertionConsumerService",
"SingleLogoutServiceUrl": "http://localhost:44380/SAML/SingleLogoutService",
"LocalCertificates": [
{
"FileName": "certificates/sp.pfx",
"Password": "password"
}
]
},
"PartnerIdentityProviderConfigurations": [
{
"Name": "https://ExampleIdentityProvider",
"Description": "Example Identity Provider",
"SignAuthnRequest": true,
"SingleSignOnServiceUrl": "https://localhost:44313/SAML/SingleSignOnService",
"SingleLogoutServiceUrl": "https://localhost:44313/SAML/SingleLogoutService",
"PartnerCertificates": [
{
"FileName": "certificates/idp.cer"
}
]
}
]
}
]
},
"PartnerName": "https://ExampleIdentityProvider"
Just to confirm, you want to add the SAML configuration dynamically?
The best way to do this is to implement the ISamlConfigurationResolver as described in the "Implementing ISamlConfigurationResolver" section of our Configuration Guide.
https://www.componentspace.com/Forums/8234/Configuration-Guide
Your implementation of ISamlConfigurationResolver is called whenever configuration is required. This means the SAML configuration is entirely dynamic.

GraphQL queries with tables join using Node.js

I am learning GraphQL so I built a little project. Let's say I have 2 models, User and Comment.
const Comment = Model.define('Comment', {
content: {
type: DataType.TEXT,
allowNull: false,
validate: {
notEmpty: true,
},
},
});
const User = Model.define('User', {
name: {
type: DataType.STRING,
allowNull: false,
validate: {
notEmpty: true,
},
},
phone: DataType.STRING,
picture: DataType.STRING,
});
The relations are one-to-many, where a user can have many comments.
I have built the schema like this:
const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: {
type: GraphQLString
},
name: {
type: GraphQLString
},
phone: {
type: GraphQLString
},
comments: {
type: new GraphQLList(CommentType),
resolve: user => user.getComments()
}
})
});
And the query:
const user = {
type: UserType,
args: {
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve(_, {id}) => User.findById(id)
};
Executing the query for a user and his comments is done with 1 request, like so:
{
User(id:"1"){
Comments{
content
}
}
}
As I understand, the client will get the results using 1 query, this is the benefit using GraphQL. But the server will execute 2 queries, one for the user and another one for his comments.
My question is, what are the best practices for building the GraphQL schema and types and combining join between tables, so that the server could also execute the query with 1 request?
The concept you are refering to is called batching. There are several libraries out there that offer this. For example:
Dataloader: generic utility maintained by Facebook that provides "a consistent API over various backends and reduce requests to those backends via batching and caching"
join-monster: "A GraphQL-to-SQL query execution layer for batch data fetching."
To anyone using .NET and the GraphQL for .NET package, I have made an extension method that converts the GraphQL Query into Entity Framework Includes.
public static class ResolveFieldContextExtensions
{
public static string GetIncludeString(this ResolveFieldContext<object> source)
{
return string.Join(',', GetIncludePaths(source.FieldAst));
}
private static IEnumerable<Field> GetChildren(IHaveSelectionSet root)
{
return root.SelectionSet.Selections.Cast<Field>()
.Where(x => x.SelectionSet.Selections.Any());
}
private static IEnumerable<string> GetIncludePaths(IHaveSelectionSet root)
{
var q = new Queue<Tuple<string, Field>>();
foreach (var child in GetChildren(root))
q.Enqueue(new Tuple<string, Field>(child.Name.ToPascalCase(), child));
while (q.Any())
{
var node = q.Dequeue();
var children = GetChildren(node.Item2).ToList();
if (children.Any())
{
foreach (var child in children)
q.Enqueue(new Tuple<string, Field>
(node.Item1 + "." + child.Name.ToPascalCase(), child));
}
else
{
yield return node.Item1;
}
}}}
Lets say we have the following query:
query {
getHistory {
id
product {
id
category {
id
subCategory {
id
}
subAnything {
id
}
}
}
}
}
We can create a variable in "resolve" method of the field:
var include = context.GetIncludeString();
which generates the following string:
"Product.Category.SubCategory,Product.Category.SubAnything"
and pass it to Entity Framework:
public Task<TEntity> Get(TKey id, string include)
{
var query = Context.Set<TEntity>();
if (!string.IsNullOrEmpty(include))
{
query = include.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Aggregate(query, (q, p) => q.Include(p));
}
return query.SingleOrDefaultAsync(c => c.Id.Equals(id));
}