Multiple Auth Handlers / Forwarding - asp.net-core

Can anyone explain in simple terms what the 'forwarding' concept means in AspNetCore authentictation ? In the source code, comments on AuthenticationSchemeOptions class explain that the ForwardAuthenticate property is "If set, this specifies the target scheme that this scheme should forward AuthenticateAsync calls to"... What exactly does that mean ? In what secnario would this happen ?

According to your source codes, you could find, it just remove the previous scheme and use the Forward scheme to authenticate the user.
/// </summary>
/// <param name="scheme">The scheme to forward. One of ForwardAuthenticate, ForwardChallenge, ForwardForbid, ForwardSignIn, or ForwardSignOut.</param>
/// <returns>The forwarded scheme or <see langword="null"/>.</returns>
protected virtual string? ResolveTarget(string? scheme)
{
var target = scheme ?? Options.ForwardDefaultSelector?.Invoke(Context) ?? Options.ForwardDefault;
// Prevent self targetting
return string.Equals(target, Scheme.Name, StringComparison.Ordinal)
? null
: target;
}
/// <inheritdoc />
public async Task<AuthenticateResult> AuthenticateAsync()
{
var target = ResolveTarget(Options.ForwardAuthenticate);
if (target != null)
{
return await Context.AuthenticateAsync(target);
}
// Calling Authenticate more than once should always return the original value.
var result = await HandleAuthenticateOnceAsync() ?? AuthenticateResult.NoResult();
if (result.Failure == null)
{
var ticket = result.Ticket;
if (ticket?.Principal != null)
{
Logger.AuthenticationSchemeAuthenticated(Scheme.Name);
}
else
{
Logger.AuthenticationSchemeNotAuthenticated(Scheme.Name);
}
}
else
{
Logger.AuthenticationSchemeNotAuthenticatedWithFailure(Scheme.Name, result.Failure.Message);
}
return result;
}
The usage for this is, if one of the old existing scheme is should be replaced by a new one, by using this option could avoid a lot of code changing.

Related

Why integration bus executes 3 times on CreateObject event and why sitename is null during outgoing synchronisation in Kentico?

I've installed a fresh new version of Kentico v12 and i'm using the basic goat template.
I would like to be able to synchronize the creation of users and updating of personal informations of those users in the frontend application with a SAP webservice.
I've added a new custom field in the user object "SAPID" and created a connector to manage the synchronization with SAP webservices.
Here is my poc code:
public class CMSIntegrationConnector : BaseIntegrationConnector
{
/// <summary>
/// Initializes the connector name.
/// </summary>
public override void Init()
{
// Initializes the connector name (must match the code name of the connector object in the system)
// GetType().Name uses the name of the class as the ConnectorName
ConnectorName = GetType().Name;
SubscribeToObjects(TaskProcessTypeEnum.AsyncSimple, PredefinedObjectType.USER);
}
public override IntegrationProcessResultEnum ProcessInternalTaskAsync(GeneralizedInfo infoObj, TranslationHelper translations, TaskTypeEnum taskType, TaskDataTypeEnum dataType, string siteName, out string errorMessage)
{
try
{
if (siteName == "DancingGoat")
{
if (infoObj.TypeInfo.ObjectType == PredefinedObjectType.USER.ToString())
{
if (taskType == TaskTypeEnum.CreateObject)
{
EventLogProvider.LogInformation("Connector", "CreateUser", "User created on SAP !!!!!");
UserInfo user = infoObj.MainObject as UserInfo;
// Call SAP webservice
user.SetValue("SAPID", Guid.NewGuid());
UserInfoProvider.SetUserInfo(user);
}
else if (taskType == TaskTypeEnum.UpdateObject)
{
EventLogProvider.LogInformation("Connector", "CreateUser", "User updated on SAP !!!!!");
// Call SAP webservice
}
}
}
}
catch (Exception ex)
{
EventLogProvider.LogException("Connector", "CreateUser", ex);
errorMessage = ex.Message;
return IntegrationProcessResultEnum.Error;
}
errorMessage = null;
return IntegrationProcessResultEnum.OK;
}
}
Here is a dump of the values of parameters I get when I debug on a createobject event:
I have 2 issues.
Why the parameter sitename is null ?
Why it's executed 3 times successively on each CreateObject event ?
I've checked this post: Kentico 12 DancingGoat MVC SiteName is empty or null
Adding "localhost" in the domain alias of the site didn't work.
Thank you by advance !
With the comment of Enn I understood that my problem came from this instruction "UserInfoProvider.SetUserInfo(user);"
I subscribed to apply a logic on any new User objects and update it again in the logic, that's why I it was executed more than once.
To solve it, I applied the proposition of Michal
using (CMSActionContext context = new CMSActionContext())
{
context.LogWebFarmTasks = false;
context.LogEvents = false;
context.LogExport = false;
context.LogIntegration = false;
context.LogSynchronization = false;
UserInfo user = infoObj.MainObject as UserInfo;
user.SetValue("SAPID", Guid.NewGuid());
UserInfoProvider.SetUserInfo(user);
}
Thank you !

Remove route from RouteCollection in Asp.Net Core and add new with same route name (nopCommerce-4.00)

I want to remove existing route from RouteCollection and want to add new route with same route name in nopCommerce 4.00 via plugin
Existing route name:
//home page
routeBuilder.MapLocalizedRoute("HomePage", "",
new { controller = "Home", action = "Index" });
I Want to replace it with
routeBuilder.MapLocalizedRoute("HomePage", "",
new { controller = "CustomPage", action = "Homepage" });
I tried several ways but not get any luck.
In my case, I have to replace the robots.txt generation.
I created a new public controller in my plugin, and I copy the original action here:
public class MiscCommonController : BasePublicController
{
#region Fields
private readonly ICommonModelFactory _commonModelFactory;
#endregion Fields
#region Ctor
public MiscCommonController(
ICommonModelFactory commonModelFactory
)
{
this._commonModelFactory = commonModelFactory;
}
#endregion Ctor
#region Methods
//robots.txt file
//available even when a store is closed
[CheckAccessClosedStore(true)]
//available even when navigation is not allowed
[CheckAccessPublicStore(true)]
public virtual IActionResult RobotsTextFile()
{
var robotsFileContent = _commonModelFactory.PrepareRobotsTextFile();
return Content(robotsFileContent, MimeTypes.TextPlain);
}
#endregion Methods
}
After this I create a RouteProvider for my plugin, and I replaced the original route to my own one.
public partial class RouteProvider : IRouteProvider
{
/// <summary>
/// Gets a priority of route provider
/// </summary>
public int Priority => -1;
/// <summary>
/// Register routes
/// </summary>
/// <param name="routeBuilder">Route builder</param>
public void RegisterRoutes(IRouteBuilder routeBuilder)
{
Route route = null;
foreach (Route item in routeBuilder.Routes)
{
if (item.Name == "robots.txt")
{
route = item;
break;
}
}
if (route != null) routeBuilder.Routes.Remove(route);
routeBuilder.MapRoute(
"robots.txt",
"robots.txt",
new { controller = "MiscCommon", action = "RobotsTextFile" }
);
}
}
That's all.
After this implementation, the routing works fine, and the get request landed in my own controller, which is act like the original.
Now, I can replace the generation logic with my own.
I hope it helps.
in the RouteProvider.cs of your plugin write these codes (based on your names):
var lastExistingRoute= routeBuilder.Routes.FirstOrDefault(x => ((Route)x).Name == "HomePage");
routeBuilder.Routes.Remove(lastExistingRoute);
routeBuilder.MapRoute("HomePage", "",
new { controller = "CustomPage", action = "Homepage", });
and the below codes worked for myself version 4.20:
var lastDownloadRoute=routeBuilder.Routes.FirstOrDefault(x => ((Route)x).Name == "GetDownload");
routeBuilder.Routes.Remove(lastDownloadRoute);
routeBuilder.MapRoute("GetDownload", "download/getdownload/{guid}/{agree?}",
new { controller = "AzTechProduct", action = "GetPayed", });
There are two potential ways to deal with this in nopCommerce 4.3 that I see with a quick examination of the code.
First, you could create an IRouteProvider, add your route that has the same signature as the one you wish to 'delete' and make sure the Priority on the provider is greater than 1.
Doing this will basically override the default route built into Nop. This is my preferred method.
public partial class RouteProvider: IRouteProvider
{
public void RegisterRoutes(IEndpointRouteBuilder endpointRouteBuilder)
{
var pattern = string.Empty;
if (DataSettingsManager.DatabaseIsInstalled)
{
var localizationSettings = endpointRouteBuilder.ServiceProvider.GetRequiredService<LocalizationSettings>();
if (localizationSettings.SeoFriendlyUrlsForLanguagesEnabled)
{
var langservice = endpointRouteBuilder.ServiceProvider.GetRequiredService<ILanguageService>();
var languages = langservice.GetAllLanguages().ToList();
pattern = "{language:lang=" + languages.FirstOrDefault().UniqueSeoCode + "}/";
}
}
// Handle the standard request
endpointRouteBuilder.MapControllerRoute("Wishlist", pattern + "wishlist/{customerGuid?}",
new { controller = "MyShoppingCart", action = "Wishlist" });
return;
}
public int Priority => 100;
}
The key to the code above is the Priority value. This route will get added to the list first and will therefore take precedence over the default route. Using this technique eliminates the need to delete the default route.
The second possible method turns out to not work because the endpointRouteBuilder.DataSources[n].Endpoints collection is read only. So, as far as I know, you can't remove mappings from that list after they have been added.

OData service with multiple routes while using unbound functions

Does anyone know how to get OData v4 hosted in a .NET service to work with multiple routes?
I have the following:
config.MapODataServiceRoute("test1", "test1", GetEdmModelTest1());
config.MapODataServiceRoute("test2", "test2", GetEdmModelTest2());
Each of the GetEdmModel methods have mapped objects.
I can get to the service as following (this is working fine):
http://testing.com/test1/objects1()
http://testing.com/test2/objects2()
But if I try to call a function like the following (will not work):
[HttpGet]
[ODataRoute("test1/TestFunction1()")]
public int TestFunction1()
{ return 1; }
It will throw the following error:
The path template 'test1/TestFunction1()' on the action 'TestFunction1' in controller 'Testing' is not a valid OData path template. Resource not found for the segment 'test1'.
Yet if I remove the "MapODataServiceRoute" for "test2" so there is only one route, it all works.
How do I get this to work with multiple routes?
** I have posted a full example of the issue at the following **
https://github.com/OData/WebApi/issues/1223
** I have tried the OData version sample listed below with the following issues **
https://github.com/OData/ODataSamples/tree/master/WebApi/v4/ODataVersioningSample
I have tried the "OData Version" example before and it did not work.
It seems that unbound (unbound is the goal) does not follow the same routing rules are normal service calls.
Ex. If you download the "OData Version" example and do the following.
In V1 -> WebApiConfig.cs add
builder.Function(nameof(Controller.ProductsV1Controller.Test)).Returns<string>();
In V2 -> WebApiConfig.cs add
builder.Function(nameof(Controller.ProductsV2Controller.Test)).Returns<string>();
In V1 -> ProductsV1Controller.cs add
[HttpGet]
[ODataRoute("Test()")]
public string Test()
{ return "V1_Test"; }
In V2 -> ProductsV2Controller.cs add
[HttpGet]
[ODataRoute("Test()")]
public string Test()
{ return "V2_Test"; }
Now call it by this. " /versionbyroute/v1/Test() " and you will get "V2_Test"
The problem is that "GetControllerName" does not know how to get the controller when it is using unbound functions / actions.
This is why most sample code I have found fails when trying to "infer" the controller.
Have a look at OData Versioning Sample for a primer.
The key point of trouble is usually that the DefaultHttpControllerSelector maps controllers by local name, not fullname/namespace.
If your entity types and therefore controller names are unique across both EdmModels you will not have to do anything special, it should just work out of the box. The above sample takes advantage of this concept by forcing you to inject a string value into the physical names of the controller classes to make them unique and then in the ODataVersionControllerSelector GetControllerName is overridden to maps the incoming route to the customised controller names
If unique names for the controllers seems to hard, and you would prefer to use the full namespace (meaning your controller names logic remains standard) then you can of course implement your own logic to select the specific controller class instance when overriding DefaultHttpControllerSelector. simply override SelectController instead. This method will need to return an instance of HttpControllerDescriptor which is a bit more involved than the sample.
To show you what I mean, I will post the solution to a requirement from an older project, that was a little bit different to yours. I have a single WebAPI project that manages access to multiple databases, these databases have similar schema, many Entity names are the same which means that those controller classes will have the same names. The controllers are structured by folders/namespaces such that there is a root folder called DB, then there is a folder for each database, then the controllers are in there.
You can see that this project has many different schemas, they effectively map to versions of an evolving solution, the non-DB namespaces in this image are a mix of OData v4, v3 and standard REST apis. It is possible to get all these beasts to co-exist ;)
This override of the HttpControllerSelector inspects the runtime once to cache a list of all the controller classes, then maps the incoming route requests by matching the route prefix to the correct controller class.
/// <summary>
/// Customised controller for intercepting traffic for the DB Odata feeds.
/// Any route that is not prefixed with ~/DB/ will not be intercepted or processed via this controller
/// <remarks>Will instead be directed to the base class</remarks>
/// </summary>
public class DBODataHttpControllerSelector : DefaultHttpControllerSelector
{
private readonly HttpConfiguration _configuration;
public DBODataHttpControllerSelector(HttpConfiguration config)
: base(config)
{
_configuration = config;
}
// From: http://www.codeproject.com/Articles/741326/Introduction-to-Web-API-Versioning
private Dictionary<string, HttpControllerDescriptor> _controllerMap = null;
private List<string> _duplicates = new List<string>();
/// <summary>
/// Because we are interested in supporting nested namespaces similar to MVC "Area"s we need to
/// Index our available controller classes by the potential url segments that might be passed in
/// </summary>
/// <returns></returns>
private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
{
if(_controllerMap != null)
return _controllerMap;
_controllerMap = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
// Create a lookup table where key is "namespace.controller". The value of "namespace" is the last
// segment of the full namespace. For example:
// MyApplication.Controllers.V1.ProductsController => "V1.Products"
IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
foreach (Type t in controllerTypes)
{
var segments = t.Namespace.Split(Type.Delimiter);
// For the dictionary key, strip "Controller" from the end of the type name.
// This matches the behavior of DefaultHttpControllerSelector.
var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);
var key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", segments[segments.Length - 2], segments[segments.Length - 1], controllerName);
// Check for duplicate keys.
if (_controllerMap.Keys.Contains(key))
{
_duplicates.Add(key);
}
else
{
_controllerMap[key] = new HttpControllerDescriptor(_configuration, t.Name, t);
}
}
// Remove any duplicates from the dictionary, because these create ambiguous matches.
// For example, "Foo.V1.ProductsController" and "Bar.V1.ProductsController" both map to "v1.products".
// CS: Ahem... thats why I've opted to go 3 levels of depth to key name, but this still applies if the duplicates are there again
foreach (string s in _duplicates)
{
_controllerMap.Remove(s);
}
return _controllerMap;
}
/// <summary>
/// Because we are interested in supporting nested namespaces we want the full route
/// to match to the full namespace (or at least the right part of it)
/// </summary>
/// <returns></returns>
private Dictionary<string, HttpControllerDescriptor> _fullControllerMap = null;
private Dictionary<string, HttpControllerDescriptor> InitializeFullControllerDictionary()
{
if(_fullControllerMap != null)
return _fullControllerMap;
_fullControllerMap = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
// Create a lookup table where key is "namespace.controller". The value of "namespace" is the last
// segment of the full namespace. For example:
// MyApplication.Controllers.V1.ProductsController => "V1.Products"
IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
foreach (Type t in controllerTypes)
{
var segments = t.Namespace.Split(Type.Delimiter);
// For the dictionary key, strip "Controller" from the end of the type name.
// This matches the behavior of DefaultHttpControllerSelector.
var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);
var key = t.FullName;// t.Namespace + "." + controllerName;
_fullControllerMap[key] = new HttpControllerDescriptor(_configuration, t.Name, t);
}
return _fullControllerMap;
}
/// <summary>
/// Select the controllers with a simulated MVC area sort of functionality, but only for the ~/DB/ route
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public override System.Web.Http.Controllers.HttpControllerDescriptor SelectController(System.Net.Http.HttpRequestMessage request)
{
string rootPath = "db";
IHttpRouteData routeData = request.GetRouteData();
string[] uriSegments = request.RequestUri.LocalPath.Split('/');
if (uriSegments.First().ToLower() == rootPath || uriSegments[1].ToLower() == rootPath)
{
#region DB Route Selector
// If we can find a known api and a controller, then redirect to the correct controller
// Otherwise allow the standard select to work
string[] knownApis = new string[] { "tms", "srg", "cumulus" };
// Get variables from the route data.
/* support version like this:
* config.Routes.MapODataRoute(
routeName: "ODataDefault",
routePrefix: "{version}/{area}/{controller}",
model: model);
object versionName = null;
routeData.Values.TryGetValue("version", out versionName);
object apiName = null;
routeData.Values.TryGetValue("api", out apiName);
object controllerName = null;
routeData.Values.TryGetValue("controller", out controllerName);
* */
// CS: we'll just use the local path AFTER the root path
// db/tms/contact
// db/srg/contact
// Implicity parse this as
// db/{api}/{controller}
// so [0] = ""
// so [1] = "api"
// so [2] = "version" (optional)
// so [2 or 3] = "controller"
if (uriSegments.Length > 3)
{
string apiName = uriSegments[2];
if (knownApis.Contains(string.Format("{0}", apiName).ToLower()))
{
string version = "";
string controllerName = uriSegments[3];
if (controllerName.ToLower().StartsWith("v")
// and the rest of the name is numeric
&& !controllerName.Skip(1).Any(c => !Char.IsNumber(c))
)
{
version = controllerName;
controllerName = uriSegments[4];
}
// if the route has an OData item selector (#) then this needs to be trimmed from the end.
if (controllerName.Contains('('))
controllerName = controllerName.Substring(0, controllerName.IndexOf('('));
string fullName = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", apiName, version, controllerName).Replace("..", ".");
// Search for the controller.
// _controllerTypes is a list of HttpControllerDescriptors
var descriptors = InitializeControllerDictionary().Where(t => t.Key.EndsWith(fullName, StringComparison.OrdinalIgnoreCase)).ToList();
if (descriptors.Any())
{
var descriptor = descriptors.First().Value;
if (descriptors.Count > 1)
{
descriptor = null;
// Assume that the version was missing, and we have implemented versioning for that controller
// If there is a row with no versioning, so no v1, v2... then use that
// if all rows are versioned, use the highest version
if (descriptors.Count(d => d.Key.Split('.').Length == 2) == 1)
descriptor = descriptors.First(d => d.Key.Split('.').Length == 2).Value;
else if (descriptors.Count(d => d.Key.Split('.').Length > 2) == descriptors.Count())
descriptor = descriptors
.Where(d => d.Key.Split('.').Length > 2)
.OrderByDescending(d => d.Key.Split('.')[1])
.First().Value;
if (descriptor == null)
throw new HttpResponseException(
request.CreateErrorResponse(HttpStatusCode.InternalServerError,
"Multiple controllers were found that match this un-versioned request."));
}
if (descriptor != null)
return descriptor;
}
if (_duplicates.Any(d => d.ToLower() == fullName.ToLower()))
throw new HttpResponseException(
request.CreateErrorResponse(HttpStatusCode.InternalServerError,
"Multiple controllers were found that match this request."));
}
}
#endregion DB Route Selector
}
else
{
// match on class names that match the route.
// So if the route is odata.tms.testController
// Then the class name must also match
// Add in an option to doing a string mapping, so that
// route otms can mapp to odata.tms
// TODO: add any other custom logic for selecting the controller that you want, alternatively try this style syntax in your route config:
//routes.MapRoute(
// name: "Default",
// url: "{controller}/{action}/{id}",
// defaults: new { controller = "Home", action = "RegisterNow", id = UrlParameter.Optional },
// namespaces: new[] { "YourCompany.Controllers" }
//);
// Because controller path mapping might be controller/navigationproperty/action
// We need to check for the following matches:
// controller.navigationproperty.actionController
// controller.navigationpropertyController
// controllerController
string searchPath = string.Join(".", uriSegments).ToLower().Split('(')[0] + "controller";
var descriptors = InitializeFullControllerDictionary().Where(t => t.Key.ToLower().Contains(searchPath)).ToList();
if (descriptors.Any())
{
var descriptor = descriptors.First().Value;
if (descriptors.Count > 1)
{
descriptor = null;
// In this mode, I think we should only ever have a single match, ready to prove me wrong?
if (descriptor == null)
throw new HttpResponseException(
request.CreateErrorResponse(HttpStatusCode.InternalServerError,
"Multiple controllers were found that match this namespace request."));
}
if (descriptor != null)
return descriptor;
}
}
return base.SelectController(request);
}
}
You can use a Custsom MapODataServiceRoute.
The below is an example from WebApiConfig.cs
The controllers are registered with the CustomMapODataServiceRoute and its a bit cumbersome having to include typeof(NameOfController) for every controller. One of my endpoints has 22 separate controllers, but thus far it's worked.
Registering Controllers - Showing two separate OData endpoints in the same project, both containing custom functions
// Continuing Education
ODataConventionModelBuilder continuingEdBuilder = new ODataConventionModelBuilder();
continuingEdBuilder.Namespace = "db_api.Models";
var continuingEdGetCourse = continuingEdBuilder.Function("GetCourse");
continuingEdGetCourse.Parameter<string>("term_code");
continuingEdGetCourse.Parameter<string>("ssts_code");
continuingEdGetCourse.Parameter<string>("ptrm_code");
continuingEdGetCourse.Parameter<string>("subj_code_prefix");
continuingEdGetCourse.Parameter<string>("crn");
continuingEdGetCourse.ReturnsCollectionFromEntitySet<ContinuingEducationCoursesDTO>("ContinuingEducationCourseDTO");
config.CustomMapODataServiceRoute(
routeName: "odata - Continuing Education",
routePrefix: "contEd",
model: continuingEdBuilder.GetEdmModel(),
controllers: new[] { typeof(ContinuingEducationController) }
);
// Active Directory OData Endpoint
ODataConventionModelBuilder adBuilder = new ODataConventionModelBuilder();
adBuilder.Namespace = "db_api.Models";
// CMS Groups
var cmsGroupFunc = adBuilder.Function("GetCMSGroups");
cmsGroupFunc.Parameter<string>("user");
cmsGroupFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
// Departments
var deptUsersFunc = adBuilder.Function("GetADDepartmentUsers");
deptUsersFunc.Parameter<string>("department");
deptUsersFunc.ReturnsCollectionFromEntitySet<ADUser>("ADUser");
var adUsersFunc = adBuilder.Function("GetADUser");
adUsersFunc.Parameter<string>("name");
adUsersFunc.ReturnsCollectionFromEntitySet<ADUser>("ADUser");
var deptFunc = adBuilder.Function("GetADDepartments");
deptFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
var instDeptFunc = adBuilder.Function("GetADInstructorDepartments");
instDeptFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
var adTitleFunc = adBuilder.Function("GetADTitles");
adTitleFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
var adOfficeFunc = adBuilder.Function("GetADOffices");
adOfficeFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
var adDistListFunc = adBuilder.Function("GetADDistributionLists");
adDistListFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
config.CustomMapODataServiceRoute(
routeName: "odata - Active Directory",
routePrefix: "ad",
model: adBuilder.GetEdmModel(),
controllers: new[] { typeof(DepartmentsController), typeof(CMSGroupsController)
});
Creating Custom Map OData Service Route
public static class HttpConfigExt
{
public static System.Web.OData.Routing.ODataRoute CustomMapODataServiceRoute(this HttpConfiguration configuration, string routeName,
string routePrefix, Microsoft.OData.Edm.IEdmModel model, IEnumerable<Type> controllers)
{
var routingConventions = ODataRoutingConventions.CreateDefault();
// Multiple Controllers with Multiple Custom Functions
routingConventions.Insert(0, new CustomAttributeRoutingConvention(routeName, configuration, controllers));
// Custom Composite Key Convention
//routingConventions.Insert(1, new CompositeKeyRoutingConvention());
return configuration.MapODataServiceRoute(routeName,
routePrefix,
model,
new System.Web.OData.Routing.DefaultODataPathHandler(),
routingConventions,
defaultHandler: System.Net.Http.HttpClientFactory.CreatePipeline( innerHandler: new System.Web.Http.Dispatcher.HttpControllerDispatcher(configuration),
handlers: new[] { new System.Web.OData.ODataNullValueMessageHandler() }));
}
}
public class CustomAttributeRoutingConvention : AttributeRoutingConvention
{
private readonly List<Type> _controllers = new List<Type> { typeof(System.Web.OData.MetadataController) };
public CustomAttributeRoutingConvention(string routeName, HttpConfiguration configuration, IEnumerable<Type> controllers)
: base(routeName, configuration)
{
_controllers.AddRange(controllers);
}
public override bool ShouldMapController(System.Web.Http.Controllers.HttpControllerDescriptor controller)
{
return _controllers.Contains(controller.ControllerType);
}
}

log4net smtpappender custom email recipients

I am able to use log4net to send logging information to an email address using the smtpappender and a Gmail account in a VB solution (Visual Studio 2010). The recipient is configured in the log4net config file, however I would like to be able to change the recipient email address dynamically.
Is it possible without having to write a custom smtpappender?
Wether the answer is yes or no, please give me an example, preferably in VB.
It's not possible, the current SmtpAppender won't allow it. But you're lucky, the SendBuffer in the SmtpAppender can be overridden, so you can easily add some behavior to it. I think your best bet is to use the LoggingEvent properties to set the recipient:
public class MySmtpAppender : SmtpAppender
{
protected override void SendBuffer(log4net.Core.LoggingEvent[] events)
{
var Recipients = events
.Where(e => e.Properties.Contains("recipient"))
.Select(e => e.Properties["recipient"])
.Distinct();
var RecipientsAsASingleLine = string.Join(";", Recipients.ToArray()); // or whatever the separator is
var PreviousTo = To;
To = RecipientsAsASingleLine;
base.SendBuffer(events);
To = PreviousTo;
}
}
You may want to change the way to select recipients, your call.
edit The tool recommended by stuartd works quite well (well, it is quite a simple class, but still):
Public Class MySmtpAppender
Inherits SmtpAppender
Protected Overrides Sub SendBuffer(events As log4net.Core.LoggingEvent())
Dim Recipients = events.Where(Function(e) e.Properties.Contains("recipient")).[Select](Function(e) e.Properties("recipient")).Distinct()
Dim RecipientsAsASingleLine = String.Join(";", Recipients.ToArray())
' or whatever the separator is
Dim PreviousTo = [To]
[To] = RecipientsAsASingleLine
MyBase.SendBuffer(events)
[To] = PreviousTo
End Sub
End Class
it is possible. see my answer in this question - copied code below
using System;
using System.IO;
using System.Web.Mail;
using log4net.Layout;
using log4net.Core;
using log4net.Appender;
namespace SampleAppendersApp.Appender
{
/// <summary>
/// Simple mail appender that sends individual messages
/// </summary>
/// <remarks>
/// This SimpleSmtpAppender sends each LoggingEvent received as a
/// separate mail message.
/// The mail subject line can be specified using a pattern layout.
/// </remarks>
public class SimpleSmtpAppender : AppenderSkeleton
{
public SimpleSmtpAppender()
{
}
public string To
{
get { return m_to; }
set { m_to = value; }
}
public string From
{
get { return m_from; }
set { m_from = value; }
}
public PatternLayout Subject
{
get { return m_subjectLayout; }
set { m_subjectLayout = value; }
}
public string SmtpHost
{
get { return m_smtpHost; }
set { m_smtpHost = value; }
}
#region Override implementation of AppenderSkeleton
override protected void Append(LoggingEvent loggingEvent)
{
try
{
StringWriter writer = new StringWriter(System.Globalization.CultureInfo.InvariantCulture);
string t = Layout.Header;
if (t != null)
{
writer.Write(t);
}
// Render the event and append the text to the buffer
RenderLoggingEvent(writer, loggingEvent);
t = Layout.Footer;
if (t != null)
{
writer.Write(t);
}
MailMessage mailMessage = new MailMessage();
mailMessage.Body = writer.ToString();
mailMessage.From = m_from;
mailMessage.To = m_to;
if (m_subjectLayout == null)
{
mailMessage.Subject = "Missing Subject Layout";
}
else
{
StringWriter subjectWriter = new StringWriter(System.Globalization.CultureInfo.InvariantCulture);
m_subjectLayout.Format(subjectWriter, loggingEvent);
mailMessage.Subject = subjectWriter.ToString();
}
if (m_smtpHost != null && m_smtpHost.Length > 0)
{
SmtpMail.SmtpServer = m_smtpHost;
}
SmtpMail.Send(mailMessage);
}
catch(Exception e)
{
ErrorHandler.Error("Error occurred while sending e-mail notification.", e);
}
}
override protected bool RequiresLayout
{
get { return true; }
}
#endregion // Override implementation of AppenderSkeleton
private string m_to;
private string m_from;
private PatternLayout m_subjectLayout;
private string m_smtpHost;
}
}
You can use log4Net.GlobalContext class.
code:
App.config
<appender name="SmtpLogAppender" type="log4net.Appender.SmtpAppender">
<to type="log4net.Util.PatternString" value="%property{SenderList}"/>
C# Code
GlobalContext.Properties["SenderList"] = "abc#xyz.com, def#xyz.com";
log4net.Config.XmlConfigurator.Configure();
It is possible, to a certain degree, to get dynamic recipient.
In the SMTP appender the replacements for To, CC, From etc is done during configuration. Not ideal (would be best if it were to calculate the values at every send) but still workable.
Re-configuring the logging is not free but is doable programatically. You can set your To field as such :
<to type="log4net.Util.PatternString" value="SomeAccountThatReceivesAll#yourCorp.com%property{MailRecipient}" />
then in your code you can set a comma separated list of recipient like this :
log4net.GlobalContext.Properties["MailRecipient"] = "SomeOtherAccount#yourCorp.com,YourCorpSupportForThisApp#yourCorp.com";
the important bit is that you force a re-configuration AFTER you set these values. The exact syntax will depend on your config strategy, we use a central config file for all the logging so in C# it would look like this :
log4net.Config.XmlConfigurator.ConfigureAndWatch("PathToYourCentralFile.xml");
and voila ! Dynamic recipient without any custom appenders !
Personally I would prefer the custom appender, less hackish as it does not require constant re-configuring if you need to change them often. But if you only need the 10 minute fix and the config for recipient does not change once started then I found this to be good enough.

C# Compact-Framework friendly command line parser

I read this question: Command Line Parser for .NET.
I thought that was what I was looking for, but the library Command Line Parser Library is not Compact framework friendly...
I REALLY don't want to write a CL parser and I have been drifting away from the real purpose of my little app because of this unfortunate trial.
Does someone know of a library that fits the compact-framework? (preferably with simplicity and functionality like the one mentioned above)
Does not matter whether version 2 or 3.5
I developed this framework, maybe it helps:
The SysCommand is a powerful cross-platform framework, to develop Console Applications in .NET. Is simple, type-safe, and with great influences of the MVC pattern.
https://github.com/juniorgasparotto/SysCommand
namespace Example.Initialization.Simple
{
using SysCommand.ConsoleApp;
public class Program
{
public static int Main(string[] args)
{
return App.RunApplication();
}
}
// Classes inheriting from `Command` will be automatically found by the system
// and its public properties and methods will be available for use.
public class MyCommand : Command
{
public void Main(string arg1, int? arg2 = null)
{
if (arg1 != null)
this.App.Console.Write(string.Format("Main arg1='{0}'", arg1));
if (arg2 != null)
this.App.Console.Write(string.Format("Main arg2='{0}'", arg2));
}
public void MyAction(bool a)
{
this.App.Console.Write(string.Format("MyAction a='{0}'", a));
}
}
}
Tests:
// auto-generate help
$ my-app.exe help
// method "Main" typed
$ my-app.exe --arg1 value --arg2 1000
// or without "--arg2"
$ my-app.exe --arg1 value
// actions support
$ my-app.exe my-action -a
This is what I'm using. I borrowed it from somewhere, but not sure where:
using System.Collections.Specialized;
using System.Text.RegularExpressions;
/// <summary>
/// Parses the command line arguments into a name/value collection
/// </summary>
public class CommandLineArgumentParser
{
#region Fields
private StringDictionary parameters;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="CommandLineArgumentParser"/> class.
/// </summary>
/// <param name="args">command-line arguments
/// </param>
public CommandLineArgumentParser(string[] args)
{
this.parameters = new StringDictionary();
Regex spliter = new Regex(#"^-{1,2}|^/|=|:", RegexOptions.IgnoreCase | RegexOptions.Compiled);
Regex remover = new Regex(#"^['""]?(.*?)['""]?$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
string parameter = null;
string[] parts;
// Valid parameters forms:
// {-,/,--}param{ ,=,:}((",')value(",'))
// Examples:
// -param1 value1 --param2 /param3:"Test-:-work"
// /param4=happy -param5 '--=nice=--'
foreach (string txt in args)
{
// Look for new parameters (-,/ or --) and a
// possible enclosed value (=,:)
parts = spliter.Split(txt, 3);
switch (parts.Length)
{
// Found a value (for the last parameter
// found (space separator))
case 1:
if (parameter != null)
{
if (!this.parameters.ContainsKey(parameter))
{
parts[0] = remover.Replace(parts[0], "$1");
this.parameters.Add(parameter, parts[0]);
}
parameter = null;
}
// else Error: no parameter waiting for a value (skipped)
break;
// Found just a parameter
case 2:
// The last parameter is still waiting.
// With no value, set it to true.
if (parameter != null)
{
if (!this.parameters.ContainsKey(parameter))
{
this.parameters.Add(parameter, "true");
}
}
parameter = parts[1];
break;
// Parameter with enclosed value
case 3:
// The last parameter is still waiting.
// With no value, set it to true.
if (parameter != null)
{
if (!this.parameters.ContainsKey(parameter))
{
this.parameters.Add(parameter, "true");
}
}
parameter = parts[1];
// Remove possible enclosing characters (",')
if (!this.parameters.ContainsKey(parameter))
{
parts[2] = remover.Replace(parts[2], "$1");
this.parameters.Add(parameter, parts[2]);
}
parameter = null;
break;
}
}
// In case a parameter is still waiting
if (parameter != null)
{
if (!this.parameters.ContainsKey(parameter))
{
this.parameters.Add(parameter, "true");
}
}
}
#endregion
#region Properties
/// <summary>
/// Gets a count of command line arguments
/// </summary>
public int Count
{
get
{
return this.parameters.Count;
}
}
/// <summary>
/// Gets the value with the given parameter name
/// </summary>
/// <param name="param">name of the parameter</param>
/// <returns>the value of the parameter</returns>
public string this[string param]
{
get
{
return this.parameters[param];
}
}
#endregion
}
http://commandline.codeplex.com/ I've used this so many times I've lost count. Maybe it works for CE. If not, it'll provide a fantastic starting point.