Using MailChimp WebHooks feature - asp.net

Can someone please provide examples of doing this in ASP.NET. We want to do some MailCHimp – internal database synchronization and plan to do this using webhooks feature but we just can’t get it work. We want to use web hooks to synch data when someone unsubscribes from mail chimp.
Another thing to address is security. How can we secure this page from being accessed by malicious users?

Here is a piece of code that works for us. This is fairly simple but it did take us some experimenting to get it to work.
if (Request.Form["type"] != null && Request.Form["type"] == "unsubscribe")
{
string email = Request.Form["data[merges][EMAIL]"];
//now you can do insert/update data in your local database
}
Check out the API documentation for more details http://apidocs.mailchimp.com/webhooks/
Regarding security you can do a ton of stuff but it depends on how deep you want to go. One thing I’d recommend is checking your IIS logs and finding which IP address/user agent is used by mail chimp to trigger web hooks and then just block this page for all other IP addresses except for this. There are probably other things you can do to additionally secure this like using page name that is not easily guessed (f3jijselife.aspx is far better than webhooks.aspx)

I just implemented this recently based on the PHP code they provided here's the skeleton... I took out the actual implementation but should be useful hopefully
public class MailChimpWebHook : IHttpHandler
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(MailChimpWebHook));
private const string Key = "xxxx";
private const string ParamKey = "key";
private const string ParamType = "type";
private const string ParamListId = "data[list_id]";
private const string ParamListIdNew = "data[new_id]";
private const string ParamEmail = "data[email]";
private const string ParamOldEmail = "data[new_email]";
private const string ParamNewEmail = "data[old_email]";
private const string ParamProfileEmail = "data[merges][EMAIL]";
private const string ParamProfileFirstName = "data[merges][FNAME]";
private const string ParamProfileLastName = "data[merges][LNAME]";
private const string ParamProfileGroups = "data[merges][INTERESTS]";
private const string TypeSubscribe = "subscribe";
private const string TypeUnsubscribe = "unsubscribe";
private const string TypeCleaned = "cleaned";
private const string TypeUpdateEmail = "upemail";
private const string TypeProfileUpdate = "profile";
public void ProcessRequest(HttpContext context)
{
Logger.Info("==================[ Incoming Request ]==================");
if (string.IsNullOrEmpty(context.Request[ParamKey]))
{
Logger.Warn("No security key specified, ignoring request");
}
else if (context.Request[ParamKey] != Key)
{
Logger.WarnFormat("Security key specified, but not correct. Wanted: '{0}' | , but received '{1}'", Key, context.Request[ParamKey]);
}
else
{
//process the request
Logger.InfoFormat("Processing a '{0}' request...", context.Request[ParamType]);
try
{
switch (context.Request[ParamType])
{
case TypeSubscribe:
Subscribe(context.Request);
break;
case TypeUnsubscribe:
Unsubscribe(context.Request);
break;
case TypeCleaned:
Cleaned(context.Request);
break;
case TypeUpdateEmail:
UpdateEmail(context.Request);
break;
case TypeProfileUpdate:
UpdateProfile(context.Request);
break;
default:
Logger.WarnFormat("Request type '{0}' unknown, ignoring.", context.Request[ParamType]);
break;
}
}
catch (Exception e)
{
Logger.Error("There was an error processing the callback", e);
}
}
Logger.Info("Finished processing request.");
}
private void UpdateProfile(HttpRequest httpRequest)
{
Logger.Info("Processing update profile request!");
#region [ sample data structure ]
// "type": "profile",
// "fired_at": "2009-03-26 21:31:21",
// "data[id]": "8a25ff1d98",
// "data[list_id]": "a6b5da1054",
// "data[email]": "api#mailchimp.com",
// "data[email_type]": "html",
// "data[merges][EMAIL]": "api#mailchimp.com",
// "data[merges][FNAME]": "MailChimp",
// "data[merges][LNAME]": "API",
// "data[merges][INTERESTS]": "Group1,Group2",
// "data[ip_opt]": "10.20.10.30"
#endregion
}
private void UpdateEmail(HttpRequest httpRequest)
{
Logger.Info("Processing update email request!");
#region [ sample data structure ]
// "type": "upemail",
// "fired_at": "2009-03-26\ 22:15:09",
// "data[list_id]": "a6b5da1054",
// "data[new_id]": "51da8c3259",
// "data[new_email]": "api+new#mailchimp.com",
// "data[old_email]": "api+old#mailchimp.com"
#endregion
}
private void Cleaned(HttpRequest httpRequest)
{
Logger.Info("Processing cleaned email request!");
#region [ sample data structure ]
// "type": "cleaned",
// "fired_at": "2009-03-26 22:01:00",
// "data[list_id]": "a6b5da1054",
// "data[campaign_id]": "4fjk2ma9xd",
// "data[reason]": "hard",
// "data[email]": "api+cleaned#mailchimp.com"
#endregion
}
private void Unsubscribe(HttpRequest httpRequest)
{
Logger.Info("Processing unsubscribe...");
#region [ sample data structure ]
// "type": "unsubscribe",
// "fired_at": "2009-03-26 21:40:57",
// "data[action]": "unsub",
// "data[reason]": "manual",
// "data[id]": "8a25ff1d98",
// "data[list_id]": "a6b5da1054",
// "data[email]": "api+unsub#mailchimp.com",
// "data[email_type]": "html",
// "data[merges][EMAIL]": "api+unsub#mailchimp.com",
// "data[merges][FNAME]": "MailChimp",
// "data[merges][LNAME]": "API",
// "data[merges][INTERESTS]": "Group1,Group2",
// "data[ip_opt]": "10.20.10.30",
// "data[campaign_id]": "cb398d21d2",
// "data[reason]": "hard"
#endregion
}
private void Subscribe(HttpRequest httpRequest)
{
Logger.Info("Processing subscribe...");
#region [ sample data structure ]
// "type": "subscribe",
// "fired_at": "2009-03-26 21:35:57",
// "data[id]": "8a25ff1d98",
// "data[list_id]": "a6b5da1054",
// "data[email]": "api#mailchimp.com",
// "data[email_type]": "html",
// "data[merges][EMAIL]": "api#mailchimp.com",
// "data[merges][FNAME]": "MailChimp",
// "data[merges][LNAME]": "API",
// "data[merges][INTERESTS]": "Group1,Group2",
// "data[ip_opt]": "10.20.10.30",
// "data[ip_signup]": "10.20.10.30"
#endregion
}
public bool IsReusable
{
get
{
return false;
}
}
}

I'm using C# WebAPI and the solution for me was to use the FormDataCollection object from the body of the POST MailChimp sends with the webhook.
using System.Net.Http.Formatting;
[HttpPost]
[Route("mailchimp/subscriber")]
public IHttpActionResult Post([FromBody] FormDataCollection data)
{
if (data != null)
{
string type = data.Get("type");
if (!string.IsNullOrWhiteSpace(type))
{
string listId = data.Get("data[list_id]");
string id = data.Get("data[id]");
string firstName = data.Get("data[merges][FNAME]");
string lastName = data.Get("data[merges][LNAME]");
string email = data.Get("data[email]");
if (!string.IsNullOrWhiteSpace(email))
{
// Do something with the subscriber
}
}
}
}

I fully support the answer by James.
However, when trying to implement a webhook myself, I have discovered that you'll also need to implement a GET method, in order to even be able to create the webhook in MailChimp.
This did the trick for me:
[HttpGet]
[HttpOptions]
public HttpResponseMessage Get()
{
return Request.CreateResponse(HttpStatusCode.OK);
}
MailChimp documentation:
https://developer.mailchimp.com/documentation/mailchimp/guides/about-webhooks/

Related

How to send a zipped file to S3 bucket from Apex?

Folks,
I am trying to move data to s3 from Salesforce using apex class. I have been told by the data manager to send the data in zip/gzip format to the S3 bucket for storage cost savings.
I have simply tried to do a request.setCompressed(true); as I've read it compresses the body before sending it to the endpoint. Code below:
HttpRequest request = new HttpRequest();
request.setEndpoint('callout:'+DATA_NAMED_CRED+'/'+URL+'/'+generateUniqueTimeStampforSuffix());
request.setMethod('PUT');
request.setBody(JSON.serialize(data));
request.setCompressed(true);
request.setHeader('Content-Type','application/json');
But no matter what I always receive this:
<Error><Code>XAmzContentSHA256Mismatch</Code><Message>The provided 'x-amz-content-sha256' header does not match what was computed.</Message><ClientComputedContentSHA256>fd31b2b9115ef77e8076b896cb336d21d8f66947210ffcc9c4d1971b2be3bbbc</ClientComputedContentSHA256><S3ComputedContentSHA256>1e7f2115e60132afed9e61132aa41c3224c6e305ad9f820e6893364d7257ab8d</S3ComputedContentSHA256>
I have tried multiple headers too, like setting the content type to gzip/zip, etc.
Any pointers in the right direction would be appreciated.
I had a good amount of headaches attempting to do a similar thing. I feel your pain.
The following code has worked for us using lambda functions; you can try modifying it and see what happens.
public class AwsApiGateway {
// Things we need to know about the service. Set these values in init()
String host, payloadSha256;
String resource;
String service = 'execute-api';
String region;
public Url endpoint;
String accessKey;
String stage;
string secretKey;
HttpMethod method = HttpMethod.XGET;
// Remember to set "payload" here if you need to specify a body
// payload = Blob.valueOf('some-text-i-want-to-send');
// This method helps prevent leaking secret key,
// as it is never serialized
// Url endpoint;
// HttpMethod method;
Blob payload;
// Not used externally, so we hide these values
Blob signingKey;
DateTime requestTime;
Map<String, String> queryParams = new map<string,string>(), headerParams = new map<string,string>();
void init(){
if (payload == null) payload = Blob.valueOf('');
requestTime = DateTime.now();
createSigningKey(secretKey);
}
public AwsApiGateway(String resource){
this.stage = AWS_LAMBDA_STAGE
this.resource = '/' + stage + '/' + resource;
this.region = AWS_REGION;
this.endpoint = new Url(AWS_ENDPOINT);
this.accessKey = AWS_ACCESS_KEY;
this.secretKey = AWS_SECRET_KEY;
}
// Make sure we can't misspell methods
public enum HttpMethod { XGET, XPUT, XHEAD, XOPTIONS, XDELETE, XPOST }
public void setMethod (HttpMethod method){
this.method = method;
}
public void setPayload (string payload){
this.payload = Blob.valueOf(payload);
}
// Add a header
public void setHeader(String key, String value) {
headerParams.put(key.toLowerCase(), value);
}
// Add a query param
public void setQueryParam(String key, String value) {
queryParams.put(key.toLowerCase(), uriEncode(value));
}
// Create a canonical query string (used during signing)
String createCanonicalQueryString() {
String[] results = new String[0], keys = new List<String>(queryParams.keySet());
keys.sort();
for(String key: keys) {
results.add(key+'='+queryParams.get(key));
}
return String.join(results, '&');
}
// Create the canonical headers (used for signing)
String createCanonicalHeaders(String[] keys) {
keys.addAll(headerParams.keySet());
keys.sort();
String[] results = new String[0];
for(String key: keys) {
results.add(key+':'+headerParams.get(key));
}
return String.join(results, '\n')+'\n';
}
// Create the entire canonical request
String createCanonicalRequest(String[] headerKeys) {
return String.join(
new String[] {
method.name().removeStart('X'), // METHOD
new Url(endPoint, resource).getPath(), // RESOURCE
createCanonicalQueryString(), // CANONICAL QUERY STRING
createCanonicalHeaders(headerKeys), // CANONICAL HEADERS
String.join(headerKeys, ';'), // SIGNED HEADERS
payloadSha256 // SHA256 PAYLOAD
},
'\n'
);
}
// We have to replace ~ and " " correctly, or we'll break AWS on those two characters
string uriEncode(String value) {
return value==null? null: EncodingUtil.urlEncode(value, 'utf-8').replaceAll('%7E','~').replaceAll('\\+','%20');
}
// Create the entire string to sign
String createStringToSign(String[] signedHeaders) {
String result = createCanonicalRequest(signedHeaders);
return String.join(
new String[] {
'AWS4-HMAC-SHA256',
headerParams.get('date'),
String.join(new String[] { requestTime.formatGMT('yyyyMMdd'), region, service, 'aws4_request' },'/'),
EncodingUtil.convertToHex(Crypto.generateDigest('sha256', Blob.valueof(result)))
},
'\n'
);
}
// Create our signing key
void createSigningKey(String secretKey) {
signingKey = Crypto.generateMac('hmacSHA256', Blob.valueOf('aws4_request'),
Crypto.generateMac('hmacSHA256', Blob.valueOf(service),
Crypto.generateMac('hmacSHA256', Blob.valueOf(region),
Crypto.generateMac('hmacSHA256', Blob.valueOf(requestTime.formatGMT('yyyyMMdd')), Blob.valueOf('AWS4'+secretKey))
)
)
);
}
// Create all of the bits and pieces using all utility functions above
public HttpRequest createRequest() {
init();
payloadSha256 = EncodingUtil.convertToHex(Crypto.generateDigest('sha-256', payload));
setHeader('date', requestTime.formatGMT('yyyyMMdd\'T\'HHmmss\'Z\''));
if(host == null) {
host = endpoint.getHost();
}
setHeader('host', host);
HttpRequest request = new HttpRequest();
request.setMethod(method.name().removeStart('X'));
if(payload.size() > 0) {
setHeader('Content-Length', String.valueOf(payload.size()));
request.setBodyAsBlob(payload);
}
String finalEndpoint = new Url(endpoint, resource).toExternalForm(),
queryString = createCanonicalQueryString();
if(queryString != '') {
finalEndpoint += '?'+queryString;
}
request.setEndpoint(finalEndpoint);
for(String key: headerParams.keySet()) {
request.setHeader(key, headerParams.get(key));
}
String[] headerKeys = new String[0];
String stringToSign = createStringToSign(headerKeys);
request.setHeader(
'Authorization',
String.format(
'AWS4-HMAC-SHA256 Credential={0}, SignedHeaders={1},Signature={2}',
new String[] {
String.join(new String[] { accessKey, requestTime.formatGMT('yyyyMMdd'), region, service, 'aws4_request' },'/'),
String.join(headerKeys,';'), EncodingUtil.convertToHex(Crypto.generateMac('hmacSHA256', Blob.valueOf(stringToSign), signingKey))}
));
system.debug(json.serializePretty(request.getEndpoint()));
return request;
}
// Actually perform the request, and throw exception if response code is not valid
public HttpResponse sendRequest(Set<Integer> validCodes) {
HttpResponse response = new Http().send(createRequest());
if(!validCodes.contains(response.getStatusCode())) {
system.debug(json.deserializeUntyped(response.getBody()));
}
return response;
}
// Same as above, but assume that only 200 is valid
// This method exists because most of the time, 200 is what we expect
public HttpResponse sendRequest() {
return sendRequest(new Set<Integer> { 200 });
}
// TEST METHODS
public static string getEndpoint(string attribute){
AwsApiGateway api = new AwsApiGateway(attribute);
return api.createRequest().getEndpoint();
}
public static string getEndpoint(string attribute, map<string, string> params){
AwsApiGateway api = new AwsApiGateway(attribute);
for (string key: params.keySet()){
api.setQueryParam(key, params.get(key));
}
return api.createRequest().getEndpoint();
}
public class EndpointConfig {
string resource;
string attribute;
list<object> items;
map<string,string> params;
public EndpointConfig(string resource, string attribute, list<object> items){
this.items = items;
this.resource = resource;
this.attribute = attribute;
}
public EndpointConfig setQueryParams(map<string,string> parameters){
params = parameters;
return this;
}
public string endpoint(){
if (params == null){
return getEndpoint(resource);
} else return getEndpoint(resource + '/' + attribute, params);
}
public SingleRequestMock mockResponse(){
return new SingleRequestMock(200, 'OK', json.serialize(items), null);
}
}
}

React-Admin can't post to ASP.Net Core API

I am trying to connect ASP.NET Core 3 api with React-Admin. So far I can list the entries from the DB and show a single record - apparently the two GET methods are working fine. (Sticking to this example)
When I try to Create a record though with a POST method I receive a 400 Bad Request and can't track the issue.
My App.js looks like this:
import simpleRestProvider from 'ra-data-simple-rest';
import realApi from './dataProvider/myDataProvider';
const dataProvider = simpleRestProvider('api/');
const App = () =>
<Admin
dashboard={Dashboard}
dataProvider={realApi}
>
<Resource name={projects.basePath} {...projects.crud} />
...
</Admin>;
I have custom dataProvider which is copied from the official react-admin tutorial
export default (type, resource, params) => {
let url = '';
const options = {
headers: new Headers({
Accept: 'application/json',
"Content-Type": 'application/json; charset=utf-8',
}),
};
let query = "";
switch (type) {
case GET_LIST: {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([
(page - 1) * perPage,
page * perPage - 1,
]),
filter: JSON.stringify(params.filter),
};
url = `${apiUrl}/${resource}?${stringify(query)}`;
break;
}
case GET_ONE:
url = `${apiUrl}/${resource}/${params.id}`;
break;
case CREATE:
url = `${apiUrl}/${resource}`;
options.method = 'POST';
options.body = JSON.stringify(params.data);
break;
case UPDATE:
url = `${apiUrl}/${resource}/${params.id}`;
options.method = 'PUT';
options.body = JSON.stringify(params.data);
break;
case UPDATE_MANY:
query = {
filter: JSON.stringify({ id: params.ids }),
};
url = `${apiUrl}/${resource}?${stringify(query)}`;
options.method = 'PATCH';
options.body = JSON.stringify(params.data);
break;
case DELETE:
url = `${apiUrl}/${resource}/${params.id}`;
options.method = 'DELETE';
break;
case DELETE_MANY:
query = {
filter: JSON.stringify({ id: params.ids }),
};
url = `${apiUrl}/${resource}?${stringify(query)}`;
options.method = 'DELETE';
break;
case GET_MANY: {
query = {
filter: JSON.stringify({ id: params.ids }),
};
url = `${apiUrl}/${resource}?${stringify(query)}`;
break;
}
case GET_MANY_REFERENCE: {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([
(page - 1) * perPage,
page * perPage - 1,
]),
filter: JSON.stringify({
...params.filter,
[params.target]: params.id,
}),
};
url = `${apiUrl}/${resource}?${stringify(query)}`;
break;
}
default:
throw new Error(`Unsupported Data Provider request type ${type}`);
}
let headers;
return fetch(url, options)
.then(res => {
headers = res.headers;
debugger
return res.json();
})
.then(json => {
switch (type) {
case GET_LIST:
case GET_MANY_REFERENCE:
if (!headers.has('content-range')) {
throw new Error(
'The Content-Range header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?'
);
}
return {
data: json,
total: parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
),
};
case CREATE:
return { data: { ...params.data, id: json.id } };
default:
return { data: json };
}
});
};
And finally there is the ASP.NET Controller:
[Route("api/[controller]")]
[ApiController]
public abstract class RaController<T> : ControllerBase, IRaController<T> where T : class, new()
{
protected readonly IDveDbContext _context;
protected DbSet<T> _table;
public RaController(IDveDbContext context)
{
_context = context;
_table = _context.Set<T>();
}
[HttpGet("{id}")]
public async Task<ActionResult<T>> Get(int id)
{
var entity = await _table.FindAsync(id);
if (entity == null)
{
return NotFound();
}
return entity;
}
[HttpPost]
public async Task<ActionResult<T>> Post(T entity)
{
_table.Add(entity);
await _context.SaveChangesAsync();
var id = (int)typeof(T).GetProperty("Id").GetValue(entity);
return Ok(await _table.FindAsync(id));
}
}
And the exact ProjectsController
[Route("api/[controller]")]
public class ProjectsController : RaController<Project>
{
private IProjectService projectService;
public ProjectsController(IProjectService projectService, IDveDbContext dbContext)
: base (dbContext)
{
this.projectService = projectService;
}
}
I've been searching around for a solution but couldn't find any. If somebody give a hint on where the problem might be or provide an example for successflly integrated ASP.Net Core with React-Admin I would be extremely thankful!
I've gotten it to work well with a .net REST API, starting with the same pieces you started with. I see a few issues in yours:
1. Use a better data provider.
The built-in provider is more of an example. It only works with an identifier of "id." zachrybaker/ra-data-rest-client allows for you to have more realistic flexibility in what your identifier is called, for existing APIs.
2. Pattern your base controller.
Your base controller needs some adjustment. It should describe what it produces (application/json) and the POST method is "Create" not "Post" since you need to differentiate between create(insert) and update (hence the 404).
I'll also note that your base method could use to describe what type if identifier, rather than assuming int, if you need that. Here's mine. I'm hoping to open source the whole thing, stay tuned.
[ApiController]
[Produces("application/json")]
public abstract class EntityWithIdControllerBase<TEntity, TReadModel, TCreateModel, TUpdateModel, TIndentifier> : BaseEFCoreAPIController
where TEntity : class, IHaveIdentifier<TIndentifier>
{
////....
////constructor....
////....
[HttpPost("")]
public async virtual Task<ActionResult<TReadModel>> Create(CancellationToken cancellationToken, TCreateModel createModel)
{
return await CreateModel(createModel, cancellationToken);
}
////....
protected virtual async Task<TReadModel> CreateModel(TCreateModel createModel, CancellationToken cancellationToken = default(CancellationToken))
{
// create new entity from model
var entity = Mapper.Map<TEntity>(createModel);
// add to data set, id should be generated
await DataContext
.Set<TEntity>()
.AddAsync(entity, cancellationToken);
// save to database
await DataContext
.SaveChangesAsync(cancellationToken);
// convert to read model
var readModel = await ReadModel(entity.Id, cancellationToken);
return readModel;
}
////....
protected virtual async Task<TReadModel> ReadModel(TIndentifier id, CancellationToken cancellationToken = default(CancellationToken))
{
var model = await IncludeTheseForDetailResponse(DataContext
.Set<TEntity>()
.AsNoTracking())
.Where(GetIdentifierEqualityFn(id))
.ProjectTo<TReadModel>(Mapper.ConfigurationProvider)
.FirstOrDefaultAsync(cancellationToken);
return model;
}
////....
#region Expressions
protected virtual Expression<Func<TEntity, bool>> IdentityExpressionFn(ExpressionStarter<TEntity> expressionStarter, JToken token)
{
return expressionStarter.Or(GetIdentifierEqualityFn(token.Value<TIndentifier>()));
}
protected abstract Expression<Func<TEntity, bool>> GetIdentifierEqualityFn(TIndentifier o);
/// <summary>
/// When you need to include related entities on the detail response, you can add them via this IQueryable by overriding this function
/// </summary>
/// <param name="queryable"></param>
/// <returns></returns>
protected virtual IQueryable<TEntity> IncludeTheseForDetailResponse(IQueryable<TEntity> queryable) => queryable;
#endregion Expressions
}
Just to let you know, the real fun begins when you start to deal with the filtering ability of react admin. That is where my "Expressions" come into play. I have a base class that can leverage expression builders and uses LinqKit for dynamic when really necessary.
Many of my controllers are Guid, so I have an intermediate controller for that:
[ApiController]
[Produces("application/json")]
public abstract class EntityWithGuidIdController<TEntity, TReadModel, TCreateModel, TUpdateModel> :
EntityWithIdControllerBase<TEntity, TReadModel, TCreateModel, TUpdateModel, Guid>
where TEntity : class, IHaveIdentifier<Guid>, new()
{
////....
////constructor....
////....
#region Expressions
/// <summary>
/// If you need to use a primay key other than "Id", then override this.
/// </summary>
/// <param name="g"></param>
/// <returns></returns>
protected override Expression<Func<TEntity, bool>> GetIdentifierEqualityFn(Guid g) => (x => x.Id == g);
protected override Expression<Func<TEntity, bool>> IdentityExpressionFn(ExpressionStarter<TEntity> expressionStarter, JToken token)
{
if (Guid.TryParse((string)token, out Guid g))
return expressionStarter.Or(GetIdentifierEqualityFn(g));
Debug.WriteLine((string)token + " Is NOT a valid Guid. why was it sent?");
return null;
}
#endregion Expressions
}
Then I can consume it as simply as this -> in this case the detail response also includes a second object:
[Route("api/[controller]")]
public class UserController : EntityWithGuidIdController<User, UserReadModel, UserCreateModel, UserUpdateModel>
{
public UserController(ZachTestContext dataContext, IMapper mapper) : base(dataContext, mapper, CacheValidInterval.OneDay)
{ }
// Include the Adloc on the detail response.
protected override IQueryable<User> IncludeTheseForDetailResponse(IQueryable<User> queryable) => queryable.Include(x => x.Adloc);
}

Scanning for beacons using universal beacon library

I am trying to implement a mobile app (on iPhone) that just scans for beacons and displays a notification for each one. I am a noob with beacons/bluetooth.
I implemented it using the universal beacon library (https://github.com/andijakl/universal-beacon) and i've attached my ios bluetooth implementation.
my problem is that i receive about 12 beacon added events even though i only have two (I assume it is picking up all my other bluetooth devices). I also only receive the local name in the advertisement_received event.
My questions are:
how do I distinguish that it is a beacon being added?
how do i get the unique id an url from the beacon? (they are kontakt beacons)
Thanks for any help.
My beacon service:
public BeaconService()
{
// get the platform-specific provider
var provider = RootWorkItem.Services.Get<IBluetoothPacketProvider>();
if (null != provider)
{
// create a beacon manager, giving it an invoker to marshal collection changes to the UI thread
_manager = new BeaconManager(provider, Device.BeginInvokeOnMainThread);
_manager.Start();
_manager.BeaconAdded += _manager_BeaconAdded;
provider.AdvertisementPacketReceived += Provider_AdvertisementPacketReceived;
}
}
My ios bluetooth implementation:
public class iOSBluetoothPacketProvider : CocoaBluetoothPacketProvider { }
public class CocoaBluetoothPacketProvider : NSObject, IBluetoothPacketProvider
{
public event EventHandler<BLEAdvertisementPacketArgs> AdvertisementPacketReceived;
public event EventHandler<BTError> WatcherStopped;
private readonly CocoaBluetoothCentralDelegate centralDelegate;
private readonly CBCentralManager central;
public CocoaBluetoothPacketProvider()
{
Debug.WriteLine("BluetoothPacketProvider()");
centralDelegate = new CocoaBluetoothCentralDelegate();
central = new CBCentralManager(centralDelegate, null);
}
private void ScanCallback_OnAdvertisementPacketReceived(object sender, BLEAdvertisementPacketArgs e)
{
AdvertisementPacketReceived?.Invoke(this, e);
}
public void Start()
{
Debug.WriteLine("BluetoothPacketProvider:Start()");
centralDelegate.OnAdvertisementPacketReceived += ScanCallback_OnAdvertisementPacketReceived;
// Wait for the PoweredOn state
//if(CBCentralManagerState.PoweredOn == central.State) {
// central.ScanForPeripherals(peripheralUuids: new CBUUID[] { },
// options: new PeripheralScanningOptions { AllowDuplicatesKey = false });
//}
}
public void Stop()
{
Debug.WriteLine("BluetoothPacketProvider:Stop()");
centralDelegate.OnAdvertisementPacketReceived -= ScanCallback_OnAdvertisementPacketReceived;
central.StopScan();
WatcherStopped?.Invoke(sender: this, e: new BTError(BTError.BluetoothError.Success));
}
}
internal class CocoaBluetoothCentralDelegate : CBCentralManagerDelegate
{
public event EventHandler<BLEAdvertisementPacketArgs> OnAdvertisementPacketReceived;
#region CBCentralManagerDelegate
public override void ConnectedPeripheral(CBCentralManager central, CBPeripheral peripheral)
{
Debug.WriteLine($"ConnectedPeripheral(CBCentralManager central, CBPeripheral {peripheral})");
}
public override void DisconnectedPeripheral(CBCentralManager central, CBPeripheral peripheral, NSError error)
{
Debug.WriteLine($"DisconnectedPeripheral(CBCentralManager central, CBPeripheral {peripheral}, NSError {error})");
}
public override void DiscoveredPeripheral(CBCentralManager central, CBPeripheral peripheral, NSDictionary advertisementData, NSNumber RSSI)
{
Debug.WriteLine($"Cocoa peripheral {peripheral}");
Debug.WriteLine($"Cocoa advertisementData {advertisementData}");
Debug.WriteLine($"Cocoa RSSI {RSSI}");
var bLEAdvertisementPacket = new BLEAdvertisementPacket()
{
Advertisement = new BLEAdvertisement()
{
LocalName = peripheral.Name,
ServiceUuids = new List<Guid>(),
DataSections = new List<BLEAdvertisementDataSection>(),
ManufacturerData = new List<BLEManufacturerData>()
},
AdvertisementType = BLEAdvertisementType.ScanResponse,
BluetoothAddress = (ulong)peripheral.Identifier.GetHashCode(),
RawSignalStrengthInDBm = RSSI.Int16Value,
Timestamp = DateTimeOffset.Now
};
//https://developer.apple.com/documentation/corebluetooth/cbadvertisementdataserviceuuidskey
//if (advertisementData.ContainsKey(CBAdvertisement.DataServiceUUIDsKey))
//{
// bLEAdvertisementPacket.Advertisement.ServiceUuids.Add(
// item: new BLEManufacturerData(packetType: BLEPacketType.UUID16List,
// data: (advertisementData[CBAdvertisement.DataServiceUUIDsKey])));
//}
//https://developer.apple.com/documentation/corebluetooth/cbadvertisementdataservicedatakey
//if (advertisementData.ContainsKey(CBAdvertisement.DataServiceDataKey))
//{
// bLEAdvertisementPacket.Advertisement.DataSections.Add(
// item: new BLEManufacturerData(packetType: BLEPacketType.ServiceData,
// data: advertisementData[CBAdvertisement.DataServiceDataKey]));
//}
//https://developer.apple.com/documentation/corebluetooth/cbadvertisementdatamanufacturerdatakey
if (advertisementData.ContainsKey(CBAdvertisement.DataManufacturerDataKey))
{
bLEAdvertisementPacket.Advertisement.ManufacturerData.Add(
item: new BLEManufacturerData(packetType: BLEPacketType.ManufacturerData,
data: (advertisementData[CBAdvertisement.DataManufacturerDataKey]
as NSData).ToArray()));
}
// Missing CBAdvertisement.DataTxPowerLevelKey
var bLEAdvertisementPacketArgs = new BLEAdvertisementPacketArgs(data: bLEAdvertisementPacket);
OnAdvertisementPacketReceived?.Invoke(this, bLEAdvertisementPacketArgs);
}
public override void FailedToConnectPeripheral(CBCentralManager central, CBPeripheral peripheral, NSError error)
{
Debug.WriteLine($"FailedToConnectPeripheral(CBCentralManager central, CBPeripheral {peripheral}, NSError {error})");
}
public override void UpdatedState(CBCentralManager central)
{
switch (central.State)
{
case CBCentralManagerState.Unknown:
Debug.WriteLine("CBCentralManagerState.Unknown");
break;
case CBCentralManagerState.Resetting:
Debug.WriteLine("CBCentralManagerState.Resetting");
break;
case CBCentralManagerState.Unsupported:
Debug.WriteLine("CBCentralManagerState.Unsupported");
break;
case CBCentralManagerState.Unauthorized:
Debug.WriteLine("CBCentralManagerState.Unauthorized");
break;
case CBCentralManagerState.PoweredOff:
Debug.WriteLine("CBCentralManagerState.PoweredOff");
break;
case CBCentralManagerState.PoweredOn:
Debug.WriteLine("CBCentralManagerState.PoweredOn");
central.ScanForPeripherals(peripheralUuids: new CBUUID[] { },
options: new PeripheralScanningOptions { AllowDuplicatesKey = true });
break;
default:
throw new NotImplementedException();
}
}
public override void WillRestoreState(CBCentralManager central, NSDictionary dict)
{
Debug.WriteLine($"WillRestoreState(CBCentralManager central, NSDictionary {dict})");
}
#endregion CBCentralManagerDelegate
}
So in case anyone is looking for this. The universal beacon library does not have an ios implementation that converts the ios packets to the universal packets. This need to be implemented.
how do I distinguish that it is a beacon being added?
I look for the Eddystone packets and if found I add to the observable list.
how do i get the unique id an url from the beacon? (they are kontakt beacons)
You need to loop through the advertisementData sent with the advertisement and create a BLEAdvertisementDataSection. copy the frame data as NSData.

Web API Return OAuth Token as XML

Using the default Visual Studio 2013 Web API project template with individual user accounts, and posting to the /token endpoint with an Accept header of application/xml, the server still returns the response in JSON:
{"access_token":"...","token_type":"bearer","expires_in":1209599}
Is there a way to get the token back as XML?
According to RFC6749 the response format should be JSON and Microsoft implemented it accordingly. I found out that JSON formatting is implemented in Microsoft.Owin.Security.OAuth.OAuthAuthorizationServerHandler internal class with no means of extension.
I also encountered the need to have token response in XML.
The best solution I came up with was to implement HttpModule converting JSON to XML when stated in Accept header.
public class OAuthTokenXmlResponseHttpModule : IHttpModule
{
private static readonly string FilterKey = typeof(OAuthTokenXmlResponseHttpModule).Name + typeof(MemoryStreamFilter).Name;
public void Init(HttpApplication application)
{
application.BeginRequest += ApplicationOnBeginRequest;
application.EndRequest += ApplicationOnEndRequest;
}
private static void ApplicationOnBeginRequest(object sender, EventArgs eventArgs)
{
var application = (HttpApplication)sender;
if (ShouldConvertToXml(application.Context.Request) == false) return;
var filter = new MemoryStreamFilter(application.Response.Filter);
application.Response.Filter = filter;
application.Context.Items[FilterKey] = filter;
}
private static bool ShouldConvertToXml(HttpRequest request)
{
var isTokenPath = string.Equals("/token", request.Path, StringComparison.InvariantCultureIgnoreCase);
var header = request.Headers["Accept"];
return isTokenPath && (header == "text/xml" || header == "application/xml");
}
private static void ApplicationOnEndRequest(object sender, EventArgs eventArgs)
{
var context = ((HttpApplication) sender).Context;
var filter = context.Items[FilterKey] as MemoryStreamFilter;
if (filter == null) return;
var jsonResponse = filter.ToString();
var xDocument = JsonConvert.DeserializeXNode(jsonResponse, "oauth");
var xmlResponse = xDocument.ToString(SaveOptions.DisableFormatting);
WriteResponse(context.Response, xmlResponse);
}
private static void WriteResponse(HttpResponse response, string xmlResponse)
{
response.Clear();
response.ContentType = "application/xml;charset=UTF-8";
response.Write(xmlResponse);
}
public void Dispose()
{
}
}
public class MemoryStreamFilter : Stream
{
private readonly Stream _stream;
private readonly MemoryStream _memoryStream = new MemoryStream();
public MemoryStreamFilter(Stream stream)
{
_stream = stream;
}
public override void Flush()
{
_stream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
return _stream.Read(buffer, offset, count);
}
public override void Write(byte[] buffer, int offset, int count)
{
_memoryStream.Write(buffer, offset, count);
_stream.Write(buffer, offset, count);
}
public override string ToString()
{
return Encoding.UTF8.GetString(_memoryStream.ToArray());
}
#region Rest of the overrides
public override bool CanRead
{
get { throw new NotImplementedException(); }
}
public override bool CanSeek
{
get { throw new NotImplementedException(); }
}
public override bool CanWrite
{
get { throw new NotImplementedException(); }
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override long Length
{
get { throw new NotImplementedException(); }
}
public override long Position
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
#endregion
}
Ok I had such a fun time trying to figure this out using OWIN I thought I would share my solution with the community, I borrowed some insight from other posts https://stackoverflow.com/a/26216511/1148288 and https://stackoverflow.com/a/29105880/1148288 along with the concepts Alexei describs in his post. Nothing fancy doing with implementation but I had a requirement for my STS to return an XML formatted response, I wanted to keep with the paradigm of honoring the Accept header, so my end point would examine that to determine if it needed to run the XML swap or not. This is what I am current using:
private void ConfigureXMLResponseSwap(IAppBuilder app)
{
app.Use(async (context, next) =>
{
if (context.Request != null &&
context.Request.Headers != null &&
context.Request.Headers.ContainsKey("Accept") &&
context.Request.Headers.Get("Accept").Contains("xml"))
{
//Set a reference to the original body stream
using (var stream = context.Response.Body)
{
//New up and set the response body as a memory stream which implements the ability to read and set length
using (var buffer = new MemoryStream())
{
context.Response.Body = buffer;
//Allow other middlewares to process
await next.Invoke();
//On the way out, reset the buffer and read the response body into a string
buffer.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(buffer))
{
string responsebody = await reader.ReadToEndAsync();
//Using our responsebody string, parse out the XML and add a declaration
var xmlVersion = JsonConvert.DeserializeXNode(responsebody, "oauth");
xmlVersion.Declaration = new XDeclaration("1.0", "UTF-8", "yes");
//Convert the XML to a byte array
var bytes = Encoding.UTF8.GetBytes(xmlVersion.Declaration + xmlVersion.ToString());
//Clear the buffer bits and write out our new byte array
buffer.SetLength(0);
buffer.Write(bytes, 0, bytes.Length);
buffer.Seek(0, SeekOrigin.Begin);
//Set the content length to the new buffer length and the type to an xml type
context.Response.ContentLength = buffer.Length;
context.Response.ContentType = "application/xml;charset=UTF-8";
//Copy our memory stream buffer to the output stream for the client application
await buffer.CopyToAsync(stream);
}
}
}
}
else
await next.Invoke();
});
}
Of course you would then wire this up during startup config like so:
public void Configuration(IAppBuilder app)
{
HttpConfiguration httpConfig = new HttpConfiguration();
//Highly recommend this is first...
ConfigureXMLResponseSwap(app);
...more config stuff...
}
Hope that helps any other lost souls that find there way to the this post seeking to do something like this!
take a look here i hope it can help how to set a Web API REST service to always return XML not JSON
Could you retry by doing the following steps:
In the WebApiConfig.Register(), specify
config.Formatters.XmlFormatter.UseXmlSerializer = true;
var supportedMediaTypes = config.Formatters.XmlFormatter.SupportedMediaTypes;
if (supportedMediaTypes.Any(it => it.MediaType.IndexOf("application/xml", StringComparison.InvariantCultureIgnoreCase) >= 0) ==false)
{
supportedMediaTypes.Insert(0,new MediaTypeHeaderValue("application/xml"));
}
I normally just remove the XmlFormatter altogether.
// Remove the XML formatter
config.Formatters.Remove(config.Formatters.XmlFormatter);
Add the line above in your WebApiConfig class...
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Web API routes
config.MapHttpAttributeRoutes();
// Remove the XML formatter
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}

Accessing OutArgument value of Receive implementation child activity within custom WF4 activity

Using VS2012/.NET 4.5 I am creating a custom activity which implements a Receive child activity (as an implementation child). The parameters are in the example below fixed to just one: OutValue of type Guid.
I really would love to access the value of incoming parameter value in ReceiveDone, because I need to work with it and transform it before returning it from the activity. Please ignore that I am currently using a Guid, it still fails to access the value with and InvalidOperationException:
An Activity can only get the location of arguments which it owns. Activity 'TestActivity' is trying to get the location of argument 'OutValue' which is owned by activity 'Wait for
workflow start request [Internal for TestActivity]'
I have tried everything I could think of, but am stupefied. There must be a way to do this very simple thing?
public class TestActivity : NativeActivity<Guid>
{
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
var content = ReceiveParametersContent.Create(new Dictionary<string, OutArgument>()
{
// How to access the runtime value of this inside TestActivity?
{"OutValue", new OutArgument<Guid>()}
});
startReceiver = new Receive()
{
DisplayName = string.Format("Wait for workflow start request [Internal for {0}]", this.DisplayName),
CanCreateInstance = true,
ServiceContractName = XName.Get("IStartService", Namespace),
OperationName = "Start",
Content = content
};
foreach (KeyValuePair<string, OutArgument> keyValuePair in content.Parameters)
{
metadata.AddImportedChild(keyValuePair.Value.Expression);
}
metadata.AddImplementationChild(startReceiver);
}
protected override void Execute(NativeActivityContext context)
{
context.ScheduleActivity(startReceiver, ReceiveDone);
}
private void ReceiveDone(NativeActivityContext context, ActivityInstance completedInstance)
{
var receive = completedInstance.Activity as Receive;
ReceiveParametersContent content = receive.Content as ReceiveParametersContent;
try
{
// This causes InvalidOperationException.
// An Activity can only get the location of arguments which it owns.
// Activity 'TestActivity' is trying to get the location of argument 'OutValue'
// which is owned by activity 'Wait for workflow start request [Internal for TestActivity]'
var parmValue = content.Parameters["OutValue"].Get(context);
}
catch (Exception)
{ }
}
private Receive startReceiver;
private const string Namespace = "http://company.namespace";
}
Use internal variables to pass values between internal activities.
Although not directly related to your code, see the example below which should give you the idea:
public sealed class CustomNativeActivity : NativeActivity<int>
{
private Variable<int> internalVar;
private Assign<int> internalAssign;
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
base.CacheMetadata(metadata);
internalVar = new Variable<int>("intInternalVar", 10);
metadata.AddImplementationVariable(internalVar);
internalAssign = new Assign<int>
{
To = internalVar,
Value = 12345
};
metadata.AddImplementationChild(internalAssign);
}
protected override void Execute(NativeActivityContext context)
{
context.ScheduleActivity(internalAssign, (activityContext, instance) =>
{
// Use internalVar value, which was seted by previous activity
var value = internalVar.Get(activityContext);
Result.Set(activityContext, value);
});
}
}
Calling the above activity:
WorkflowInvoker.Invoke<int>(new CustomNativeActivity());
Will output:
12345
Edit:
In your case your OutArgument will be the internalVar
new OutArgument<int>(internalVar);
You need to use OutArgument and them to variables. See the code example with the documentation.
I may have tried everything I thought of, but I am stubborn and refuse to give up, so I kept on thinking ;)
I here have changed my example to use a Data class as a parameter instead (it does not change anything in itself, but I needed that in my real world example).
This code below is now a working example on how to access the incoming data. The use of an implementation Variable is the key:
runtimeVariable = new Variable<Data>();
metadata.AddImplementationVariable(runtimeVariable);
And the OutArgument:
new OutArgument<Data>(runtimeVariable)
I can then access the value with:
// Here dataValue will get the incoming value.
var dataValue = runtimeVariable.Get(context);
I haven't seen an example elsewhere, which does exactly this. Hope it will be of use to any one but me.
The code:
[DataContract]
public class Data
{
[DataMember]
Guid Property1 { get; set; }
[DataMember]
int Property2 { get; set; }
}
public class TestActivity : NativeActivity<Guid>
{
public ReceiveContent Content { get; set; }
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
runtimeVariable = new Variable<Data>();
metadata.AddImplementationVariable(runtimeVariable);
Content = ReceiveParametersContent.Create(new Dictionary<string, OutArgument>()
{
{"OutValue", new OutArgument<Data> (runtimeVariable)}
});
startReceiver = new Receive()
{
DisplayName = string.Format("Wait for workflow start request [Internal for {0}]", this.DisplayName),
CanCreateInstance = true,
ServiceContractName = XName.Get("IStartService", Namespace),
OperationName = "Start",
Content = Content
};
metadata.AddImplementationChild(startReceiver);
}
protected override void Execute(NativeActivityContext context)
{
context.ScheduleActivity(startReceiver, ReceiveDone);
}
private void ReceiveDone(NativeActivityContext context, ActivityInstance completedInstance)
{
// Here dataValue will get the incoming value.
var dataValue = runtimeVariable.Get(context);
}
private Receive startReceiver;
private Variable<Data> runtimeVariable;
private const string Namespace = "http://company.namespace";
}

Resources