Scenario:
I need to load work items from two project management systems (VSTS and Jira) through their REST API. Each of them has different authorization mechanism:
VSTS: Using access token that load from database, varied by team/group of users
Jira: Using personal token that load from user claims, varied by user
Basic classes and an interface:
public interface IProjectManagementClient
{
ProjectManagementType Type { get; }
Task<WorkItem> GetWorkItemAsync(ProjectManagement projectManagement, int id, CancellationToken token = default(CancellationToken));
Task<List<WorkItem>> GetAllWorkItemsAsync(ProjectManagement projectManagement, string term, int limit = 10, CancellationToken token = default(CancellationToken));
}
public enum ProjectManagementType
{
VSTS,
Jira
}
public class ProjectManagement
{
public int Id { get; set; }
public ProjectManagementType Type { get; set; }
public string BaseAddress { get; set; }
public string AccessToken { get; set; }
}
Setup:
services.AddHttpClient<IProjectManagementClient, VstsClient>();
services.AddHttpClient<IProjectManagementClient, JiraClient>()
.AddHttpMessageHandler(provider => new BasicAuthHandler(provider.GetService<IHttpContextAccessor>(), CustomClaimTypes.TrackerToken));
Usage example:
public TimesheetsController(IEnumerable<IProjectManagementClient> projectManagementClients)
{
_projectManagementClients = projectManagementClients;
}
public void DoWork()
{
foreach (var timesheet in _timesheets)
{
var projectManagementClient = _projectManagementClients.First(o => o.Type == timesheet.ProjectManagement.Type);
timesheet.WorkItem = timesheet.WorkItemId.HasValue
? await projectManagementClient.GetWorkItemAsync(timesheet.ProjectManagement, timesheet.WorkItemId.Value, token)
: null;
}
}
Issues:
Even though I've only registered the BasicAuthHandler to the JiraClient. It has been applied to both VSTS and Jira clients.
Thus it has overridden the VSTS authorization that has been set previously in VstsClient.
Both works fine if I only setup and use only of them at a time. Please help.
JiraClient gets overriden by VstsClient because they are both registered on the same (null) name, which is default. Try to use named registrations:
services.AddHttpClient<IProjectManagementClient, VstsClient>("VSTS");
services.AddHttpClient<IProjectManagementClient, JiraClient>("JIRA")
.AddHttpMessageHandler(provider => new BasicAuthHandler(
provider.GetService<IHttpContextAccessor>(),
CustomClaimTypes.TrackerToken));
In this case IEnumerable<IProjectManagementClient> should give two items, configured as above, without override.
Related
Out-of-box Acumatica has Twilio and Amazon SNS providers to send SMS. However, we have a business relationship with different provider (Plivo for example) and would like to utilize them for SMS service. Is it possible to use different provider?
Yes, it is possible to use different provider to send SMS.
In Acumatica, SMS service is used for
sending access code during two-factor authentication
sending business notifications
Acumatica ERP provides set of interfaces to implement SMS provider for sending SMS message.
1. PX.SmsProvider.ISmsProvider
Classes implementing this interface are automatically discovered by Acumatica ERP and are available for selection in the Provider Type box on the SMS Provider (SM203535) screen. Classes must be part of Library (DLL).
Define class implementing PX.SmsProvider.ISmsProvider interface and implement methods of ISmsProvider interface.
public class MySmsProvider : ISmsProvider
{
public IEnumerable<PXFieldState> ExportSettings
{
// Implement definition of each setting/parameter and add to settings list
get
{
return new List<PXFieldState>();
}
}
public void LoadSettings(IEnumerable<ISmsProviderSetting> settings)
{
// Retrieve value of each setting/parameter and assign to corresponding member variable
}
public async Task SendMessageAsync(SendMessageRequest request, CancellationToken cancellation)
{
// Implement logic to send SMS
}
}
2. PX.SmsProvider.ISmsProviderFactory
Class implementing constructor to initialize provider. And public properties to hold Name and Description for this provider – the way you need it to be displayed in Provider Type box on the SMS Provider (SM203535) screen.
Define class implementing PX.SmsProvider.ISmsProviderFactory interface and implement methods and properties of ISmsProviderFactory interface.
public class MySmsProviderFactory : ISmsProviderFactory
{
//Create Provider and initialize with settings
public ISmsProvider Create(IEnumerable<ISmsProviderSetting> settings)
{
var provider = new MySmsProvider();
provider.LoadSettings(settings);
return provider;
}
public ISmsProvider Create()
{
var provider = new MySmsProvider();
return provider;
}
public string Description { get; } = "My Provider";
public string Name { get; } = typeof(MySmsProvider).FullName;
}
Below example illustrates creating SMS Provider using Plivo Service.
In C# class library project, add references of PX.Common.dll, PX.Data.dll and PX.SmsProvider.Core.dll from your Acumatica Site’s bin folder.
Definition of PlivoSmsProvider class implementing PX.SmsProvider.ISmsProvider interface:
We will need Auth ID, Auth Token and From Number parameters to work with Plivo. So, we will set them up in ExportSettings method and assign them to member variables in LoadSettings method. And we will implement logic to send SMS in SendMessageAsync.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using PX.Data;
namespace PX.SmsProvider.Plivo
{
public class PlivoSmsProvider : ISmsProvider
{
#region DetailIDs const
private const string AuthID_DetailID = "AUTH_ID";
private const string AuthToken_DetailID = "AUTH_TOKEN";
private const string FromPhoneNbr_DetailID = "FROM_PHONE_NBR";
#endregion
#region DetailID_Display const
private const string AuthID_DetailID_Display = "Auth ID";
private const string AuthToken_DetailID_Display = "Auth Token";
private const string FromPhoneNbr_DetailID_Display = "From Number";
#endregion
private string m_AuthID;
public string AuthID { get { return m_AuthID; } }
private string m_AuthToken;
public string AuthToken { get { return m_AuthToken; } }
private string m_FromPhoneNbr;
public string FromPhoneNbr { get { return m_FromPhoneNbr; } }
public IEnumerable<PXFieldState> ExportSettings
{
get
{
var settings = new List<PXFieldState>();
var authID = (PXStringState)PXStringState.CreateInstance(
m_AuthID,
null,
false,
AuthID_DetailID,
null,
1,
null,
null,
null,
null,
null
);
authID.DisplayName = AuthID_DetailID_Display;
settings.Add(authID);
var authToken = (PXStringState)PXStringState.CreateInstance(
m_AuthToken,
null,
false,
AuthToken_DetailID,
null,
1,
"*",
null,
null,
null,
null
);
authToken.DisplayName = AuthToken_DetailID_Display;
settings.Add(authToken);
var fromPhoneNbr = (PXStringState)PXStringState.CreateInstance(
m_FromPhoneNbr,
null,
false,
FromPhoneNbr_DetailID,
null,
1,
null,
null,
null,
null,
null
);
fromPhoneNbr.DisplayName = FromPhoneNbr_DetailID_Display;
settings.Add(fromPhoneNbr);
return settings;
}
}
public void LoadSettings(IEnumerable<ISmsProviderSetting> settings)
{
foreach (ISmsProviderSetting detail in settings)
{
switch (detail.Name.ToUpper())
{
case AuthID_DetailID: m_AuthID = detail.Value; break;
case AuthToken_DetailID: m_AuthToken = detail.Value; break;
case FromPhoneNbr_DetailID: m_FromPhoneNbr = detail.Value; break;
}
}
}
public async Task SendMessageAsync(SendMessageRequest request, CancellationToken cancellation)
{
// implement logic to send SMS
}
}
}
Definition of PlivoSmsProviderFactory class implementing PX.SmsProvider.ISmsProviderFactory interface.
using System.Collections.Generic;
namespace PX.SmsProvider.Plivo
{
public class PlivoSmsProviderFactory : ISmsProviderFactory
{
public ISmsProvider Create(IEnumerable<ISmsProviderSetting> settings)
{
var provider = new PlivoSmsProvider();
provider.LoadSettings(settings);
return provider;
}
public ISmsProvider Create()
{
var provider = new PlivoSmsProvider();
return provider;
}
public string Description { get; } = "Plivo SMS Provider";
public string Name { get; } = typeof(PlivoSmsProvider).FullName;
}
}
Once this library is published via customization, this New provider will be available in SMS Providers (SM203535) screen.
Download Acumatica Source code and Customization deployment package
I have a API which connects to my dynamo db. My API has quite a few endpoints for GET, POST, Delete etc. I am using the following code:
var awsCredentials = Helper.AwsCredentials(id, password);
var awsdbClient = Helper.DbClient(awsCredentials, "us-east-2");
var awsContext = Helper.DynamoDbContext(awsdbClient);
List<ScanCondition> conditions = new List<ScanCondition>();
var response = await context.ScanAsync<MyData>(conditions).GetRemainingAsync();
return response.ToList();
The first three lines of my code ie setting awsCredentials, awsdbClient & awsContext are repeated in each of my WEB API call.
And this is my static helper class:
public static class Helper
{
public static BasicAWSCredentials AwsCredentials(string id, string password)
{
var credentials = new BasicAWSCredentials(id, password);
return credentials;
}
public static AmazonDynamoDBClient DynamoDbClient(BasicAWSCredentials credentials, RegionEndpoint region)
{
var client = new DBClient(credentials, region);
return client;
}
public static DynamoDBContext DynamoDbContext(AmazonDynamoDBClient client)
{
var context = new DynamoDBContext(client);
return context;
}
}
I use this helper class in my API to initialize AWS.
Is there a better way to initialize this?
Let's take advantage of ASP.Net's built-in Dependency Injection.
We need to make a quick interface to expose the values you need.
public interface IDynamoDbClientAccessor
{
DynamoDBContext GetContext();
}
And a settings class that we'll use in a bit.
public class DynamoDbClientAccessorSettings
{
public string Id { get; set; }
public string Password { get; set; }
public string Region { get; set; }
}
Now the concrete class.
public class DynamoDbClientAccessor : IDynamoDbClientAccessor
{
private readonly DynamoDbClientAccessorSettings settings;
public DynamoDbClientAccessor(IOptions<DynamoDbClientAccessorSettings> options)
{
settings = options?.Value ?? throw new ArgumentNullException(nameof(options));
}
public DynamoDBContext GetContext()
{
// You have the option to alter this if you don't
// want to create a new context each time.
// Have a private variable at the top of this class
// of type DynamoDBContext. If that variable is not null,
// return the value. If it is null, create a new value,
// set the variable, and return it.
var awsCredentials = Helper.AwsCredentials(settings.Id, settings.Password);
var awsdbClient = Helper.DbClient(awsCredentials, settings.Region);
var awsContext = Helper.DynamoDbContext(awsdbClient);
return awsContext;
}
}
Hook all of this up in your Startup class
services.AddSingleton<IDynamoDbClientAccessor, DynamoDbClientAccessor>();
services.Configure<DynamoDbClientAccessorSettings>(c =>
{
c.Id = "YOUR ID";
c.Password = "YOUR PASSWORD";
c.Region = "YOUR REGION";
});
Now in your controller or other DI service you ask for a IDynamoDbClientAccessor instance in the constructor.
Once you get more familar with Dependency Injection you'll be able to break apart more things into their own dependent services. As Daniel says, the AWS SDK even provides some interfaces for you to use which can help as well.
I have a base class called ServicePluginBase that implements logging.
public class PluginLog
{
public int Id { get; set; }
public int? ServiceId { get; set; }
public string Event { get; set; }
public string Details { get; set; }
public DateTime DateTime { get; set; }
public string User { get; set; }
}
public class SQLPluginLogger : IPluginLogger
{
//EFLogginContext maps PluginLog like so:
// modelBuilder.Entity<PluginLog>().ToTable("log").HasKey(l => l.Id)
private EFLoggingContext _logger = new EFLoggingContext();
public IQueryable<PluginLog> LogItems
{
get { return _logger.LogItems; }
}
public void LogEvent(PluginLog item)
{
_logger.LogItems.Add(item);
_logger.SaveChanges();
}
}
public abstract class ServicePluginBase : IPlugin
{
private IPluginLogger _logger;
public ServicePluginBase(IPluginLogger logger)
{
_logger = logger;
}
protected LogEvent(string eventName, string details)
{
PluginLog _event = new PluginLog()
{
ServiceId = this.Id,
Event = eventName,
Details = details,
User = Thread.CurrentPrincipal.Identity.Name,
DateTime = DateTime.Now
};
_logger.LogEvent(_event);
}
}
Now, within my concrete class, I log events as they happen. In one class, I have some asynchronous methods running -- and logging. Sometimes this works great. Other times, I get the error stating that "Property 'Id' is part of the object's key and cannot be updated." Interestingly enough, I have absolutely no code that updates the value of Id and I never do Updates of log entries -- I only Add new ones.
Here is the async code from one of my plugins.
public class CPTManager : ServicePluginBase
{
public override async Task HandlePluginProcessAsync()
{
...
await ProcessUsersAsync(requiredUsersList, currentUsersList);
}
private async Task ProcessUsersAsync(List<ExtendedUser> required, List<ExtendedUser> current)
{
using (var http = new HttpClient())
{
var removals = currentUsers.Where(cu => !required.Select(r => r.id).Contains(cu.id)).ToList();
await DisableUsersAsync(removals http);
await AddRequiredUsersAsync(requiredUsers.Where(ru => ru.MustAdd).ToList());
}
}
private async Task DisableUsersAsync(List<ExtendedUser> users, HttpClient client)
{
LogEvent("Disable Users","Total to disable: " + users.Count());
await Task.WhenAll(users.Select(async user =>
{
... //Http call to disable user via Web API
string status = "Disable User";
if(response.status == "ERROR")
{
EmailFailDisableNotification(user);
status += " - Fail";
}
LogEvent(statusText, ...);
if(response.status != "ERROR")
{
//Update FoxPro database via OleDbConnection (not EF)
LogEvent("ClearUDF", ...);
}
}));
}
private async Task AddRequiredUsersAsync(List<ExtendedUser> users, HttpClient client)
{
LogEvent("Add Required Users", "Total users to add: " + users.Count());
await Task.WhenAll(users.Select(async user =>
{
... //Http call to add user via web API
LogEvent("Add User", ...);
if(response.status != "ERROR")
{
//Update FoxPro DB via OleDBConnection (not EF)
LogEvent("UDF UPdate",...);
}
}));
}
}
First, I'm confused why I'm getting the error mentioned above -- "Id can't be updated" ... I'm not populating the Id field nor am I doing updates to the Log file. There are no related tables -- just the single log table.
My guess is that I'm doing something improperly in regards to asynchronous processing, but I'm having trouble seeing it if I am.
Any ideas as to what may be going on?
Imagine a WCF service running on IIS. It has one method which returns one type:
namespace TheServer
{
[ServiceContract]
public interface IServerSideInterface
{
[OperationContract]
ServerSideResultType CreateParentData(ServerSideParameterType input);
}
}
However, on the client I wish to have:
namespace TheClient
{
[ServiceContract]
public interface IClientSideInterface
{
[OperationContract]
ClientSideResultType CreateParentData(ClientSideParameterType input);
}
}
It's actually slightly more complicated as I want it to be asynchronous, but one step at a time.
I wish to use a ChannelFactory to communicate from the client to the server.
It is here I am stuck.
The next bit of code uses the type names I'm using in my sample.
...
private readonly IClientWcfServiceChannel _client;
public ChanFacWcfServiceMainPageViewModel()
{
var f = new ChannelFactory<IClientWcfServiceChannel>(new BasicHttpBinding(),
new EndpointAddress("http://localhost:50001/WcfService.svc"));
_client = f.CreateChannel();
FireCommand = new RelayCommand(Execute);
}
private void Callback(IAsyncResult ar)
{
var result2 = _client.EndCreateParentData(ar);
//var result = ((IClientWcfService)ar.AsyncState).EndCreateParentData(ar);
Result = result2.ToString();
}
private void Execute()
{
_client.BeginCreateParentData(ClientWcfServiceStartUpMode.StartUpLater, Callback, SynchronizationContext.Current);
}
This gives me a "not found" exception in the Callback method.
How do I map from the server type to the client type? They are essentially identical except for the names. On the server everything starts "Server" and on the client, the types were copy and pasted and renamed with "Client" at the start. The namespace also.
I do not want to use a shared type in a library common to both projects and I do not want to use svcUtil or "Add service reference" to create proxies (although I have to poke at their code).
Additional info:
VS 2012 + 4.5.
Ok, so I've got it to work. There were a number of issues mixing together to make it more complicated than I expected. The end result uses a Silverlight client. Silverlight has some specific restrictions in that you have to use async calls for services. It throws an exception if you try to access a synchronous called service.
EDIT: Added some DataMember attributes.
So on the server I annotated the interfaces and classes:
[ServiceContract(Name = "MyServiceClass", Namespace = "Ian.Server")]
public interface IServerWcfService
{
[OperationContract]
ServerWcfServiceParentData CreateParentData(ServerWcfServiceStartUpMode mode);
}
[DataContract(Name = "ServiceChildData", Namespace = "Ian.Server")]
public class ServerWcfServiceParentData
{
[DataMember]
public IEnumerable<ServerWcfServiceChildData> Children { get; private set; }
}
[DataContract(Name = "ServiceChildData", Namespace = "Ian.Server")]
public class ServerWcfServiceChildData
{
[DataMember]
public string ChildName { get; set; }
[DataMember]
public ServerWcfServiceChildData NestedChild { get; set; }
[DataMember]
public string Text { get; set; }
}
[DataContract(Name = "ServiceStartUpMode", Namespace = "Ian.Server")]
public enum ServerWcfServiceStartUpMode
{
[EnumMember(Value = "None")]
None,
[EnumMember(Value = "StartUpNow")]
StartUpNow,
[EnumMember(Value = "StartUpLater")]
StartUpLater
}
On the client I created the same classes but with my new names and similar annotations:
[ServiceContract(Name = "MyServiceClass", Namespace = "Ian.Server")]
public interface IClientWcfService
{
[OperationContract(AsyncPattern = true, Action = "Ian.Server/MyServiceClass/CreateParentData",
ReplyAction = "Ian.Server/MyServiceClass/CreateParentDataResponse")]
IAsyncResult BeginCreateParentData(ClientWcfServiceStartUpMode mode, AsyncCallback callback, object asyncState);
ClientWcfServiceParentData EndCreateParentData(IAsyncResult result);
}
public interface IClientWcfServiceChannel : IClientWcfService, IClientChannel
{
}
[DataContract(Name = "ServiceChildData", Namespace = "Ian.Server")]
public class ClientWcfServiceParentData
{
[DataMember]
public IEnumerable<ClientWcfServiceChildData> Children { get; set; }
}
[DataContract(Name = "ServiceChildData", Namespace = "Ian.Server")]
public class ClientWcfServiceChildData
{
[DataMember]
public string ChildName { get; set; }
[DataMember]
public ClientWcfServiceChildData NestedChild { get; set; }
[DataMember]
public string Text { get; set; }
}
[DataContract(Name = "ServiceStartUpMode", Namespace = "Ian.Server")]
public enum ClientWcfServiceStartUpMode
{
[EnumMember(Value = "None")]
None,
[EnumMember(Value = "StartUpNow")]
StartUpNow,
[EnumMember(Value = "StartUpLater")]
StartUpLater
}
Notice the async changes for the service contract. We have a Begin and End pair with the async flag set. Only the Begin has an OperationContract attribute.
Also I set the Action and ReplyAction to the values I found in the wsdl.
I have a viewmodel in my Silverlight App, the important parts for calling the service are here:
private string _result;
private readonly IClientWcfServiceChannel _client;
public ChanFacWcfServiceMainPageViewModel()
{
var f = new ChannelFactory<IClientWcfServiceChannel>(new BasicHttpBinding(),
new EndpointAddress("http://localhost:50001/WcfService.svc"));
_client = f.CreateChannel();
FireCommand = new RelayCommand(Execute);
}
private void Callback(IAsyncResult ar)
{
var context = ar.AsyncState as SynchronizationContext;
if (context == null)
{
throw new Exception("wtf");
}
var result2 = _client.EndCreateParentData(ar);
context.Post(o => { Result = result2.ToString(); }, null);
}
private void Execute()
{
_client.BeginCreateParentData(ClientWcfServiceStartUpMode.StartUpLater, Callback, SynchronizationContext.Current);
}
It's quite scrappy still, passing the SyncContext around and so on, but it does work.
For this example it just returns the type name to the view, which is pointless but proves it has a) returned something and b) that the type is the type I expected.
Importantly there is no shared code. Nothing exists in a common Portable library for example.
I have a model Administrator that has its properties, but it also consists of numerous static methods that do not really tied any way to the current object itself like for example GetByCredentials(string username, string password);. Is it somehow possible to divide static methods someplace else and lave object as pure as possible?
Example
public class Administrator : Entity
{
// OBJECT START
public int Id { get; set; }
public DateTime CreatedDateTime { get; set; }
public DateTime UpdatedDateTime { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string PasswordSalt { get; set; }
public void SetNewPassword(string password)
{
var cryptoService = new PBKDF2();
this.Password = cryptoService.Compute(password);
this.PasswordSalt = cryptoService.Salt;
}
public override void OnBeforeInsert()
{
this.CreatedDateTime = DateTime.Now;
this.UpdatedDateTime = DateTime.Now;
this.SetNewPassword(this.Password);
}
public override void OnBeforeUpdate()
{
this.UpdatedDateTime = DateTime.Now;
}
// OBJECT END
// Now I have multiple static methods that do not really
// have anything to do with current object
public static Administrator GetByCredentials(string username, string password)
{
var db = new MainDataContext();
var admin = db.Administrators.SingleOrDefault(x => x.Username == username);
if (admin == null) return null;
ICryptoService cryptoService = new PBKDF2();
var hash = cryptoService.Compute(password, admin.PasswordSalt);
if (hash == admin.Password) return admin;
return null;
}
public static bool IsCurrentIpBanned
{
get
{
const int minutesBlocked = 5;
const int maxLoginCount = 5;
var db = new MainDataContext();
var loginCount = db.AdministratorAuthorizationLogs.AsEnumerable().Count(x => x.Ip == HttpContext.Current.Request.UserHostAddress && x.CreatedDateTime.AddMinutes(minutesBlocked) > DateTime.Now && x.IsSuccess == false);
return loginCount > maxLoginCount;
}
}
public static void LogSuccess(Administrator admin)
{
Administrator.Log(admin, true);
}
public static void LogFailure(Administrator admin)
{
Administrator.Log(admin, false);
}
private static void Log(Administrator admin, bool success)
{
var db = new MainDataContext();
db.AdministratorAuthorizationLogs.Add(new AdministratorAuthorizationLog
{
Username = admin.Username,
Password = admin.Password,
Ip = HttpContext.Current.Request.UserHostAddress,
IsSuccess = success
});
db.SaveChanges();
}
}
There are several options here, but the main thing is that C# classes are the tool for separating concerns.
The most obvious is to capture those things in their own abstraction(s). For example, the GetByCredentials might be better as a (non-static) member of a different class Authority or similar. That class only needs to be able to create an Administrator type.
You can also use extension methods. A possible candidate for that is Log, which takes an Administrator as an argument and uses only public facilities on it. Extension methods are defined in a separate class, but allow you to use them "as if" they were members of the extended class, e.g.:
public static class AdministratorExtensions
{
public static void log( this Administrator admin, bool success ) { ... }
}
var admin = new Administrator();
admin.Log( true );
The key thing is to identify real abstractions and build your system up from them by combining them in sensible ways. Separating out concerns is part of that process.
This is a hint that your class "knows too much". The Administrator class should only know what concerns an administrator. He shouldn't be able to query the database and retrieve entities.
You should look into the repository pattern. Try to decompose your application into multiple layers. For example, you could have a DataRepository class whose main concern is to query and update the database entities.